Labeled arguments can be thought of as length-1 records, which, like length-1 tuples, don't need enclosing braces/parens. This analogy would work better if labeled arguments used only 1 colon instead of 2. But maybe there are parsing conflict demons hiding.
You can be certain that I've tried just about every possible combination in order to try to achieve this.
I want this more than anything else for Reason.
I would even be willing to swap type and labeled arguments in order to achieve it (using :: for types like Haskell).
I believe we can pull it off even without swapping type and label, if we require that type annotations always include a spaces surrounding the colon such as (x : y).
We would make an exception for let bindings like:
let x: int = 10;
But of course, we would always print them with surrounding spaces to encourage always having spaces around the type constraint colon.
Aside from that, I have a completely separate proposal for labelled arguments that removes the pitfalls of optional arguments, and uses a "reverse colon label" :name instead of name:. It could be possibly combined with what you're describing as well (regular name:).
let greeting (:name n, :age a) => "hello " ++ n;
let msg = greeting (:name "Joe", :age 29);
/* Look no error about optionals being erased */
let greeting (:prefix p = "!", :name n, :age a) => p ++ "hello " ++ n;
let msg = greeting (:name "Joe", :age 29);
let msg = greeting (:prefix "!!", :name "Joe", :age 29);
To partially apply, you would use the old syntax (or we create another syntax for partial application - this isn't the crux of the proposal).
let greetingToBob = greeting name::"bob";
This syntax allows punning (both at call site and definition site), type annotations, default arguments, eliminates the pitfalls of named arguments by always ensuring there is at least one final non-named argument both at definition time and application time.
Here's an example where I provide a type annotation on the optional argument, and where the other arguments are punned, and where age is annotated with a type:
let greeting (:prefix p="!", :name, :age : int) => p ++ "hello " ++ n;
let prefix = "_";
let name = "_";
let age = 99;
let lookImPunningAtTheCallSite = greeting (:prefix, :name, :age);
I believe we can pull it off if we require that type annotations always include a spaces surrounding the colon
such as (x : y).
Sounds like a win to me.
To partially apply, you would use the old syntax.
I don't think that I have internalized the whole proposal, but a different syntax for partial application is a bit worrisome. Maybe worth it.
I believe we can pull it off if we require that type annotations always include a spaces surrounding the colon
such as (x : y).
Looks weird to me when the first space in (x : y) is making a syntax difference while the second doesn't.
Josh: one consolation is that partial application of named args is pretty rare today and it really is just at fundamental odds with optional arguments - this proposal makes the syntax weird for the rare thing that is anyways at odds with something really great that I'd like to encourage use of (optionals). Right now I want to encourage optionals to noobs but I always have to include a complicated word of warning.
Josh: one consolation is that partial application of named args is pretty rare today and it really is just at
fundamental odds with optional arguments - this proposal makes the syntax weird for the rare thing that is
anyways at odds with something really great that I'd like to encourage use of (optionals). Right now I want
to encourage optionals to noobs but I always have to include a complicated word of warning.
Fair point. I have used partial application of optional args, but it's a tricky and fragile mess of interspersing enough mandatory args to make it work.
On a related point, it would be good if labeled args were essentially zero-complication, in the sense of not screwing up partial application, type inference, or anything. If reordering them needs to be sacrificed, fine. If making the arg mandatory is required, fine. But a cheap way to label the args, at least in the type signature, really helps. I realize that that's another whole tangent down a rabbit hole, sorry.
When I read the entire write up for reason, the named arguments bit was the most confusing. The double colons remind me of list concatenation. Plus the default values and calling these methods all gets a little confusing. I don't really have a solution in mind yet, but I'll keep thinking.
As for Jordan's proposal, I like the :name syntax, but the example you gave was a little confusing as it looks like a tuple:
I propose that instead of this:
let greeting (:name n, :age a) => "hello " ++ n;
let msg = greeting (:name "Joe", :age 29);
We do this:
let greeting (:name n) (:age a) => "hello " ++ n;
let msg = greeting (:name "Joe") (:age 29)
The example for the docs:
let add = fun first::f second::s => f + s;
let result = add first::10 second::20;
let addFour = add second::4;
let five = addFour first::1;
/* Notice how `by` is to the left of `num` */
let increment = fun by::by=0 num => num + by;
let two = increment by::1 1;
let four = increment 4;
would become:
let add = fun (:first f) (:second s) => f + s;
let result = add (:first 10) (:second 20);
let addFour = add (:second 4);
let five = addFour (:first 1);
/* Notice how `by` is to the left of `num` */
let increment = fun (:by by=0) num => num + by;
let two = increment (:by 1) 1;
let four = increment 4;
It does add extra parens though, which may become a pain to write, but it's more readable IMO.
Plus, I think you can add inline type annotations for documentation if you need to:
let increment = fun (:by by:int=0) num => num + by;
I agree with @nmn about the comma being very confusing to me, as nothing but tuples uses comma (which is awesome btw).
This example from @jordwalke let greeting (:prefix p="!", :name, :age : int) => p ++ "hello " ++ n; kills it for me. Overloading : that much seems unnecessarily confusing.
This example from @nmn let increment = fun (:by by:int=0) num => num + by; is also pretty confusing.
I'd say that out of everything proposed here, I prefer @nmn's last proposal. let add = fun (:first f) (:second s) => f + s; feels very clojure-y and fits in this pattern of tagging values (like tagging types with variants) that seems to be very used in ML world.
But I have a question. What's wrong with OCaml's approach of using ~?
let add = fun (:first f) (:second s) => f + s;
let result = add (:first 10) (:second 20);
let addFour = add (:second 4);
let five = addFour (:first 1);
/* Notice how `by` is to the left of `num` */
let increment = fun (:by by=0) num => num + by;
let two = increment (:by 1) 1;
let four = increment 4;
becomes
let add = fun (~first f) (~second s) => f + s;
let result = add (~first 10) (~second 20);
let addFour = add (~second 4);
let five = addFour (~first 1);
/* Notice how `by` is to the left of `num` */
let increment = fun (~by = 0) num => num + by;
let two = increment (~by 1) 1;
let four = increment 4;
and
let increment = fun (:by by:int=0) num => num + by;
becomes
let increment = fun (~by = 0):int num:int => num + by;
and from @jordwalke's proposal
let greeting (:prefix p="!", :name, :age : int) => p ++ "hello " ++ n;
let prefix = "_";
let name = "_";
let age = 99;
let lookImPunningAtTheCallSite = greeting (:prefix, :name, :age);
becomes
let greeting (~prefix p="!") ~name ~age:int => p ++ "hello " ++ n;
let prefix = "_";
let name = "_";
let age = 99;
let lookImPunningAtTheCallSite = greeting ~prefix ~name ~age;
Type annotations are on the outside of the inner expression, and you can see (~name ...) as an expression (maybe ?).
let someFunc
singleArg
singleArgWithTypeAnnotation : int /* spaces around the colon aren't necessary */
~namedArg
(~namedArgWithDefault = 10)
~namedArgWithType : int
(~argWithInternalName bla)
(~sameAsLastTwoArgs bla2 = 10)
(~sameAsLastThreeButWithTypeAnnotations bla3 = 10) : int => {
singleArg + singleArgWithTypeAnnotation + namedArg + namedArgWithDefault + namedArgWithType + bla + bla2 + bla3
}
I might be missing something obvious, and maybe this is ambiguous from the parser's perspective, but this looks the best to me because coming from JS world that doesn't have named args, it's ok to introduce a new symbol. And then as soon as you see ~ you can think "oh yeah named arg". Same as as soon as you see , you think "oh yeah a tuple".
Thanks for the thoughtful replies. For any proposal for named args, please create a version of this gist that shows how the proposal handles all of the scenarios:
https://gist.github.com/jordwalke/6935739b74eb9b8496670cc7860f5acf
Note that cases where you see name::name are supposed to be punned, but the current :: syntax does not support punning.
The commas in the proposal I listed above are what allow punning and type annotations. The tradeoff is that, well, you need commas and parens.
Current named arguments in OCaml are difficult to learn and I've gotten feedback from OCaml programmers that they can never seem to remember the syntax for anything other than simple cases (the hard-to-remember cases are optionals with or without defaults etc).
The current syntax in Reason, although I consider it a placeholder, is nice because you can learn and remember it very easily. The pattern is:
name::regularArgSyntax
or
name::regularArgSyntax=default
Where default is either the default value or ? if no default. regularArgSyntax is everything you already know about arguments.
I'd like to have something that is as easy to remember, and can also support punning. It's less important to support punning at application time, IMHO - but more important to support it at function definition time (but ideally we'd have both).
Another nice property of the current named args is that the types of them look like the function definitions, which isn't the case in OCaml. I would just never write the types of them in OCaml because I couldn't remember how to do so. With Reason the types are just like the values:
let namedAlias a::aa b::bb => aa + bb;
type namedAlias = a::int => b::int => int;
let myOptional a::a=? b::b=? () => 10;
type myOptional = a::int? => b::int? => unit => int;
It's less important to support punning at application time, IMHO -
but more important to support it at function definition time
I don't understand the reasoning on this point. When punning at
application sites is supported, then it encourages using the labels as
local variable names in the code surrounding the call site. This has
the effect of making the "vocabulary" for a function more consistent
across all of its call sites. If anything, I would think that this is
more important than punning in the function definition itself, as
there will only be one name there anyhow.
I have to say, the first time I saw argument "punning", I was very confused. It didn't make sense to me that func ~arg was sugar for func ~arg:arg. Because in my head I pictured ~arg as being the whole named argument, the label used to identify the expression that I'm about to pass in. So doing func ~arg just seemed nonsensical.
I did get used it and now enjoy the brevity though.
The current syntax in Reason, although I consider it a placeholder, is nice because you can learn and remember it very easily
That's true. Took us about 1min to look it up and remember :: and slap it everywhere.
I made https://gist.github.com/bsansouci/499a5b2eede84303e2fd961a67535551. I hope this helps. I'm considering making that change in the parser and test it on our little project to see how it goes because looking at it like this, it's hard to tell how bad it is compared to the simplicity of no punning ::.
@jberdine My logic was that when a function declares an argument count, to that implementation it's almost always the count. When calling a function, the function's count is likely my calling function's appNumberThatHappensToBeWhatIWantToPassToCount.
Here's an example that demonstrates what I was thinking:
let deleteUser userId::userId => {
/* Do the delete */
};
let verifyValidUser userId::userId => {
/* Do the verification */
};
...
let userThatPostsSpam = getSpamUser ();
let userThatPostsGoodThings = getGoodUser ();
deleteUser userId::userThatPostsSpam;
verifyValidUser userId::userThatPostsGoodThings;
To both deleteUser and verifyValidUser, the userId is the userId, and the function's purpose usually removes the need to alias the argument to something more specific.
But when calling a function, usually the data more frequently has a name that takes into account the context of the calling function, which is used in many places, passed to _many_ functions and has a name that is specific to how it is used inside the calling function.
Ideally punning would be achieved in both places, but it seems one is more useful than the other.
I suppose that my view is that labeled arguments are largely pointless if there are only a few arguments which are all distinguished by type, and the function's name makes the argument roles clear. So if things are clear at the call site and the local names don't match, just omit the labels. It is only if there are many, or multiple arguments of the same type, where the overhead of labels is a win. In those cases, it is at the call sites where potential confusion is dangerous, and so being a bit more verbose to make the names match is useful. People who actually like the style of ObjC code probable have a completely different position.
Update: I've discovered a really easy way to implement punning for the current syntax. It doesn't fix all the issues in named arguments, but we can at least add it very easily to the current named arguments syntax right now while we come up with a syntax that is even bette. For now, we can have ::field be a pun for field::field.
To be honest, if our current named argument syntax supported punning (both at call site and definition site), I might even be able to live with it longer term (it's much easier to learn than OCaml's).
The ::punned syntax is now supported in master! It's not the perfect named argument syntax, but it definitely makes the current one more enjoyable.
Fixed in master
Most helpful comment
I don't understand the reasoning on this point. When punning at
application sites is supported, then it encourages using the labels as
local variable names in the code surrounding the call site. This has
the effect of making the "vocabulary" for a function more consistent
across all of its call sites. If anything, I would think that this is
more important than punning in the function definition itself, as
there will only be one name there anyhow.