Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(compiler)!: Labeled and default arguments #1623

Merged
merged 12 commits into from
Mar 14, 2023
Merged

Conversation

ospencer
Copy link
Member

@ospencer ospencer commented Jan 29, 2023

Closes #388

Draft because this needs tests, code cleanup, parser messages updates, and as always, because things may still change 🙂

Optional and Labeled Arguments

This PR introduces the ability to provide default values for function arguments and optionally omit those arguments when calling the function. Additionally, this PR allows regular arguments to be supplied by name if desired. If you are familiar with how "keyword arguments" work in Python, you'll find this to be fairly similar.

Declaring an argument as optional

For an argument to be omitted by the caller, a default value must be provided.

let rec join = (separator=" ", strings) => {
  match (strings) {
    [] => "",
    [string] => string,
    [hd, ...tl] => hd ++ separator ++ join(separator=separator, tl)
  }
}

The join function here can be called without the separator argument, and the default value will be used instead:

join(["foo", "bar", "baz"]) // "foo bar baz"

We can optionally provide our own separator:

join(separator=", ", ["foo", "bar", "baz"]) // "foo, bar, baz"

And we can provide the separator argument in any position:

join(["foo", "bar", "baz"], separator=", ") // "foo, bar, baz"

Type signatures

The type signature of join is (?separator: String, strings: List<String>) -> String. The ? before separator indicates that this parameter is optional. The name of the parameter strings is also included in the type signature.

Because the parameter name strings is also included in the type signature, we can also provide that parameter with its label:

join(separator=", ", strings=["foo", "bar", "baz"]) // "foo, bar, baz"

And of course, we can provide these in whatever order we like:

join(strings=["foo", "bar", "baz"], separator=", ") // "foo, bar, baz"

Typechecking

Two function types are considered equal if, with labels removed, the types are equal. This is to allow function authors to name their function parameters whatever they like and still be able to pass them as arguments to other functions. For example, even if List.map had type (fn: (value: a) -> b, list: List<a>) -> List<b>, your mapper function would not need to call its argument value.

A function that accepts an optional argument does not typecheck with a function that does not accept that optional argument, even if all other types are equal.

If a function type is inferred by arguments that are supplied positionally, they are unlabeled.

@ospencer ospencer self-assigned this Jan 29, 2023
@alex-snezhko
Copy link
Member

This is awesome! Apologies in advance for the syntax bikeshedding 😅, but personally I find the

let fn = (label reference) => ...

syntax where the label comes first a bit unclear; if I were reading a function defined like this I feel I would very likely have to double-take to remember which one is the label and which is the reference (I even had to double-take to make sure I didn't mix them up after writing the example above 😆). In the community meeting I believe you brought up doing the label as reference syntax, which in my opinion is clearer since as in the context of pattern matching assigns the pattern on the left side of the as to the binding on the right side. Is there a reason you decided against as?

@ospencer
Copy link
Member Author

Yeah, that's totally fair, this syntax comes from Swift.

The thing that sucks about using as here is that it works completely differently than every other place in the language. When importing a module and you want to rename it, you use as. When you want to change the name of a value on import, you use as. When you want to create a binding while writing a pattern, you use as. When you want to change the name of a value when you export, you use as.

But in this one case, you'd be using as to provide a destructuring pattern, which is just very very different than all of the cases I listed above. It gets worse when you see them next to each other:

let foo = (pair as (fst, snd) as myTuple) => ...

That first as says that I'm providing a pattern for this label, whereas the second says I'm providing an alias for my pattern.

I don't think the Swift syntax is perfect, but I do think it's better than overloading as.

Certainly still open to suggestions for syntax 😄

@ospencer
Copy link
Member Author

Something else that I thought of that I probably shouldn't bring up due to the sheer level of bikeshedding this could cause:

Some time ago we decided that we were going to use double colon :: for type annotations instead of single colon. If we did, we could still use single colon for argument labels (like we still would for records).

The syntax could then be:

let foo = (label: pattern) => ...

And of course still just

let foo = (label) => ...

for short.

@ospencer
Copy link
Member Author

Which would be separate from

let foo = (label :: Type) => ...

and distinguish

let foo = (label: pattern :: Type) => ...

@alex-snezhko
Copy link
Member

I see, assuming that explicit typing is changed to :: then that would be a nicer syntax imo. If that is done then maybe applying arguments should also be done with a colon?

let fn = (label: pattern) => ...
fn(label: 1)

@ospencer
Copy link
Member Author

@alex-snezhko Decided to drop the requirement that labels be required, so it avoids the whole issue entirely. Argument labels are just inferred, and this covers 95% of cases just fine. The only time it's less than ideal is when an argument label is unable to be inferred (such as doing a destructure) in a module signature, but you can do (fst, snd) as pair and pair becomes the label (it's just not required that the user do this). Some tooling (like Graindoc and the lsp) will likely warn that it's good practice to have all provided values have explicit labels.

Doing it this way keeps the language complexity down tremendously, and I believe now this is a non-breaking change.

@alex-snezhko
Copy link
Member

Awesome!

@ospencer ospencer changed the title feat(compiler)!: Optional and labeled arguments feat(compiler)!: Labeled and default arguments Feb 16, 2023
@ospencer ospencer force-pushed the oscar/optional-args branch 2 times, most recently from 24e01d7 to 167202d Compare February 21, 2023 23:30
@ospencer ospencer force-pushed the oscar/optional-args branch 5 times, most recently from e932a6b to 7b372c1 Compare March 7, 2023 05:49
@ospencer ospencer marked this pull request as ready for review March 7, 2023 05:50
Copy link
Member

@phated phated left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple questions and some cleanup requests. I also made some suggestions on the error messages because I'd prefer to not use optional in the error messages.

compiler/src/formatting/format.re Outdated Show resolved Hide resolved
compiler/src/parsing/ast_helper.re Outdated Show resolved Hide resolved
compiler/src/parsing/parser.mly Outdated Show resolved Hide resolved
compiler/src/typed/typecore.re Outdated Show resolved Hide resolved
compiler/src/typed/typecore.re Outdated Show resolved Hide resolved
compiler/src/typed/typecore.re Outdated Show resolved Hide resolved
compiler/src/typed/typecore.re Outdated Show resolved Hide resolved
compiler/src/typed/typecore.re Outdated Show resolved Hide resolved
compiler/src/typed/typecore.re Outdated Show resolved Hide resolved
compiler/test/suites/functions.re Show resolved Hide resolved
Copy link
Member

@phated phated left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's GO!

@phated phated added this pull request to the merge queue Mar 14, 2023
Merged via the queue into main with commit 28a38ac Mar 14, 2023
@phated phated deleted the oscar/optional-args branch March 14, 2023 00:38
av8ta pushed a commit to av8ta/grain that referenced this pull request Apr 11, 2023
* feat(compiler)!: Labeled and default arguments

* graindocs

* remove formatting fixme

* make loc required

* Use helper for argument types

* impossible case instead of assert false

* use label name for generated options

* use more standard user unaccessible name

* standardize around call over apply

* rework error messages

* Add out of order use of argument in default value test

* add order test
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Lang] Named (Optional) arguments
4 participants