Type for function with optional named args different?

Hi all!
So when you make type annotations on a function with optional named args, you are required to be explicit with the option in the argument type:

    let reduce = ( 
          ~onChange: option<F.t => unit>=?,
          ~onComplete: option<F.t => unit>=?,
          cha,
        ) => {
          .....
        }

but when you separate that type, the option must be left off

  type reduce = (
    ~onChange: F.t => unit=?,
    ~onComplete: F.t => unit=?,
    F.change,
  ) => unit

Is that right? I would like to see those be the same.

Thanks
Alex

1 Like

I would recommend to not annotate the implementation. Only annotate the interface. This will make the difference a moot question.

3 Likes

This is correct. It is because inline type annotations for arguments are what the inside of the function “sees.” When you type an entire function at once, then you’re typing what the outside of the function sees.

Outside of that function, you have to pass a non-option value to onChange. As soon as your value goes inside the function, it’s automatically wrapped into an option.

It’s unfortunate that this can be confusing, although I concur that it’s best to only annotate the interface anyway in general.

6 Likes

I have heard this response a number of times now in rescript and I cant caution enough against it.
If this language does not comfortably and reliably support the types that are its first feature, it will fail.

still seems like the inner signature could have the same form as the outer signature with the option wrapping being implied?

[maybe parenthetically] I hear lots of the grey beards here talk about interface files and I can guarantee that no new conscripts to the language ever touch them. Noone in my team has the slightest idea about them after a year and a half. It is very common though for people to give a binding an explicit type to narrow the location of a type error…and I hope that would be natural for any type without excuse

Another thought on inside/outside is that if thats the approach, then the =? element should not be allowed inside as thats not a concern inside the function?

That’s why I am suggesting using the language in the way that it comfortably and reliably supports. Types in interface files and annotation-free implementation code. It’s the best of both worlds.

Also, there are small particularities in the type system that can confuse newcomers, as you found in this case. Taking my suggestion and not annotating your implementation would have avoided hitting this corner case altogether. The type in the interface would look exactly as you expected it to. It’s because you went slightly against the grain of the language and tried to annotate the implementation, that you ran into this slight confusion.

Interface files have myriad other benefits: faster to read because you don’t have to get sidetracked by implementation details; allow controlling exported items (how are you controlling encapsulation and privacy now?), obvious place to put doc comments without littering the implementation code.

2 Likes

So implementation files should not allow type annotations?
This is a completely different language than the one presented over all =)

Let’s look at an analogy. If someone were to come to me with this code:

type person = {firstName: string, secondName: option<string>}

let fullName = person => {
  let fn = person.firstName

  if Belt.Option.isSome(person.secondName) {
    let sn = Belt.Option.getExn(person.secondName)
    `${fn} ${sn}`
  } else {
    `${fn}`
  }
}

I would tell them to rewrite it in a more idiomatic way:

let fullName = ({firstName, secondName}) =>
  switch secondName {
  | Some(sn) => `${firstName} ${sn}`
  | None => firstName
  }

// EDIT: or:

let fullName = person => switch person {
  | {firstName, secondName: Some(sn)} => `${firstName} ${sn}`
  | {firstName, secondName: None} => firstName
}

Does that mean that ReScript doesn’t allow the first way? No, it’s just that over time we learn the idioms and follow the best practices.

2 Likes

It’s because the language conflates optional arguments with optional types, and the implementation used by the language essentially leaks in the difference between .res and .resi. There’s indeed a little magic used by the compiler to handle external calls which shows through here.

Probably a good idea to make a note of this for uncurried calls (CC @Hongbo here) in case this confusion will be eliminated when (if) transitioning to uncurried by default.

1 Like

I see no deep reasons to make it more consistent?

type reduce = (
    ~onChange: option<F.t => unit>=?,
    ~onComplete: option<F.t => unit>=?,
    F.change,
  ) => unit

cons:

  • more verbose
  • breaking changes

Could it be the other way round, i.e., in the implementation, ~onChange: F.t => unit=? would be enough to convey that onChange is actually option<F.t => unit>?

1 Like

No, please. I want to be able explicitly pass None

1 Like

I do a bunch of this too, I don’t think the semantics would change at all in any case.