Hacker News new | past | comments | ask | show | jobs | submit login

I'm really impressed by the syntax. I have yet to find a piece of syntax I wouldn't like. Labelled arguments for example are delightful:

    pub fn replace(
      in string: String,
      each pattern: String,
      with replacement: String,
    ) {
      // The variables `string`, `pattern`, and `replacement` are in scope here
    }

    replace(in: "A,B,C", each: ",", with: " ")



The only part that rubs me the wrong way is,

> The pipe operator will first check to see if the left hand value could be used as the first argument to the call, e.g. a |> b(1, 2) would become b(a, 1, 2).

Optimizing for one less _ typed is a really bad trade-off in terms of clarity here.


Gleam did used to have this, but the community collectively decided that it was actually quite annoying in practice to always have to write `_, ` after every function


I actually agree, this is the most questionable piece. I prefer (and have argued for in JS) Hack-style pipes, where the value "placement" always has to be specified.

That allows arbitrary expressions on the RHS of the pipe, so for example this would be valid Gleam:

pub fn main() { io.println( "Hello" |> (_ <> " world") ) }


Gleam has curried functions by default so F# style pipes make sense imo.


Gleam doesn't have auto currying! Often folks find this (currying) a bit confusing to wrap their head around when learning functional programming and we don't feel like it really affords you much that couldn't already be achieved with just a little bit more work by the programmer.

Applicative builder APIs are the only thing we've found would be much much better if we had auto currying.


Ah my mistake. So this is a deviation from Elm?


I think you are confusing Gleam with Gren (a fork of Elm)?


swift has that - it's smart. helps with documentation, and simplifies refactoring. I'm up for any features that reduce changes upstream.


For me personally that's the part of syntax I don't like and don't understand the value of.


It separates "names as public-facing API design decisions" from "names as developer conveniences for expressing intent and clarifying their code".

These are often very at odds. And if you don't want to repeat yourself twice with two identical names... you don't have to.

And unlike Smalltalk, in Swift at least, externally unnamed but internally named is easy (just make the external name _).


> It separates "names as public-facing API design decisions" from "names as developer conveniences for expressing intent and clarifying their code"

How often is this an issue?

> And unlike Smalltalk, in Swift at least, externally unnamed but internally named is easy (just make the external name _).

So, visual clutter for very little gain. IMO


> How often is this an issue?

In our opinion, almost always.


As wikipedia would say, [citation needed].

Having shipped code in nearly two dozen different programming languages, there were very few times when I wanted my internal parameter name to be different from my external parameter name. The reasons are very simple:

- if your function is simple, you just use the params immediately, like in your `replace` example. It doesn't matter what they are called as long as their names make sense

- if your function is not simple, the parameters are usually immediately transformed into something else (mapped to different data, split to different data etc.), and their original name doesn't matter.

So this leaves a small weird case of "somehow parameter names don't make sense for the caller of the function", and I can't for the life of me come up with such an example (the example with replace in Gleam docs is not convincing, to say the least).


Two dozen programming languages shipped in or not, almost no languages support this.

It is something that you start appreciating a lot more once you have used it significantly. It helps a lot with writing more self-documenting interfaces that are legible at a glance.

Another detail I did not mention is that _public names are part of the interface_. This means that two functions that differ only in the public names are distinct overloads.

To give an extremely simple example (taken from Apple's docs):

class Counter {

    var count = 0

    func increment() {
        count += 1
    }
    
    func increment(by amount: Int) {
        count += amount
    }
    ...
}

This starts becoming even more valuable when you have more complex objects, and allows you to move (e.g. on some kind of repository class) mangling of overload names into more readable named argument overloads.


Generally stems from the philosophy that code is read more than written, and this helps readability.


Does it? Instead of a single parameter you now have two names, and a type info to boot

    pub fn replace(
      in string: String,
      each pattern: String,
      with replacement: String,
    ) {
      // The variables `string`, `pattern`, and `replacement` are in scope here
    }

    replace(in: "A,B,C", each: ",", with: " ")
vs

    pub fn replace(
      in: String,
      each: String,
      with: String,
    ) {

    }

    replace(in: "A,B,C", each: ",", with: " ")


When argument variable names and labels are combined you have two problems:

1. Labels are accidental. If all arguments automatically create labels then the programmer has not considered and deliberately designed the API. Gleam is designed to make sure that APIs are always carefully thought about and designed as appropriate.

2. Renaming a variable becomes a breaking change. We don't believe that renaming a variable should ever result in a semver major version bump.


> Labels are accidental. If all arguments automatically create labels then the programmer has not considered and deliberately designed the API.

1. Not every code is an API. And APIs exist in multiple languages that don't have labels

2. If you wanted named parameters, you could go the C# way: https://learn.microsoft.com/en-us/dotnet/csharp/programming-...

3. Since Gleam doesn't enforce labels or check labels in any way, it doesn't "make sure APIs are carefully thought out". Labels will be just as random and accidental as parameter names following the whims of the programmer who decides to use them

> Renaming a variable becomes a breaking change.

So does renaming a label.


1. Every class/method/function/whatever declaration is an API, doesn't matter if it's internal or which language it's written. 2. Why do you think it's better? It looks like it tries to cater to everyone, and leads to inconsistent API usage and requires teams to agree on linting rules and use tools to enforce consistent coding standards. 3. It's a tool for _you_ to design good APIs that are clear. Languages without named arguments don't give you this tool.

> So does renaming a label.

In languages without this feature, any renaming breaks the API, unlike the ones with distinct internal and external names. This is not the same.


the idea is that within the function body you would rather refer to the variables as e.g. `for s in string.match(pattern)` than `for s in in.match(each)`


Then call them that.

`replace(string, pattern)` is just as (if not more) readable as `replace(in, each)`.

I was specifically replying to the claim about readability.


Wouldn't you have replace("A,B,C", ",", " "), which is quite unreadable.



FYI Swift did this years ago.


Swift probably did it for ObjC compatibility, and ObjC got it from Smalltalk.


How does this work if you rename a variable? Does it break all callers?


In this example, the local variables are `string`, `pattern`, and `replacement`, and are implementation details; only the names `in`, `each`, and `with` are part of the public API. Renaming the local variables doesn't break anyone. Renaming the labels would break callers, just as changing the names of keyword arguments in other languages.


Why are there both labels and variable names?


They serve different purposes.

Without labels—i.e., with only traditional positional arguments—the example would simply be a function taking three string parameters:

    replace(String, String, String)
Of course, the documentation should specify which arguments are which, but still at call sites it would be easy to accidentally permute the arguments:

    replace("https://[^/]*", "", url)  // wrong!
(In this case, the problem is exacerbated since different languages/libraries pick different orders here! For instance, the incorrect ordering above is correct for Python's `re.sub`, so it's easy to see how mistakes might arise.)

Labels solve this problem by making the binding explicit at the call site:

    replace(each: "https://[^/]*", with: "", in: url)  // now correct!
(Looking at the Gleam docs [1], it seems that labeled arguments "can be given in any order".)

Now, how does the implementation of this function look? The obvious first approach is to require the labels to be the same as the local variable names, but this often leads to awkward "grammar", because (in the convention of Gleam and some other languages, like those in the Smalltalk/ObjC/Swift heritage) good label names tend to be more like prepositions, whereas variable names like to be nouns:

    pub fn replace(in: String, each: String, with: String) {
        // variables `in`, `each`, and `with` in scope...
        in.split(each).intersperse(with).join()  // hmm...
    }
Now, of course, the implementation can simply re-bind its arguments to new names:

    pub fn replace(in: String, each: String, with: String) {
        let string = in, pattern = each, replacement = with;
        string.split(pattern).intersperse(replacement).join()  // better.
    }
And labeled arguments are sugar for precisely that:

    pub fn replace(in string: String, each pattern: String, with replacement: String) {
        string.split(pattern).intersperse(replacement).join()
    }
[1]: https://gleam.run/book/tour/functions.html#labelled-argument...


Isn't this solvable just by using types and instances rather than just calling top level/static functions? If you have a "replace" function off of String and use a Pattern type instead of a raw string, the compiler will enforce the correct type and ordering.

    val p = Pattern("https://[^/]*")
    val in = "https://google.com/some/path"
    val path = in.replace(p, "") // /some/path
In fact this specific example is solved just by having a dedicated type, for example URI:

    val path = URI("https://google/com/some/path").path; // /some/path

This feels like to me an answer in search of a problem.


Javascript has supported this for many years.


It definitely hasn't, JavaScript doesn't even have named arguments.


If you pass your arguments as an object they are named and you can rename them. So for all practical purposes that's the same thing.


It doesn't have labelled arguments. It can roughly simulate them using an object, but there's a slight performance cost to doing so.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: