gleam-lang / suggestions

📙 A place for ideas and feedback
26 stars 2 forks source link

Generate guards for external functions that take primitives #88

Open QuinnWilton opened 3 years ago

QuinnWilton commented 3 years ago

Given the following external:

pub external fn reverse_list(List(a)) -> List(a) = "lists" "reverse"

I think we could generate a function that asserts that the input is a list:

reverse_list(A) when is_list(A) ->
    lists:reverse(A).

With a bit more work, we could probably also assert that the function returns a list:

reverse_list(A) when is_list(A) ->
    case lists:reverse(A) of
      B when is_list(B) -> B
    end.

I think this would go a long way toward offering safer interop between Gleam and Erlang, by immediately crashing when a runtime type error occurs at the boundary between the systems.

At the very least, this is something I've been doing manually, in the opposite direction, when calling Gleam from Elixir.

lpil commented 3 years ago

It seem like the guard be used when writing a function in Erlang, importing it into Gleam as a public function, and then calling it from Erlang. Why would one not call the original Erlang version from Erlang in this case? If called from Gleam the guards are made redundant by the type system, and if the type annotations were incorrect then the guards would also be incorrect.

QuinnWilton commented 3 years ago

If called from Gleam the guards are made redundant by the type system, and if the type annotations were incorrect then the guards would also be incorrect.

I realize now that adding guards to the arguments doesn't make any sense, because Gleam is already typechecking those. It could still be valuable to add guards for the return value though, to detect cases where the return value of the function is either incorrectly or insufficiently annotated.

lpil commented 3 years ago

I'm not sure I'm fully understanding. Could you describe a situation in which this would catch a problem? Thank you

On Sat, 12 Sep 2020, 00:44 Quinn Wilton, notifications@github.com wrote:

If called from Gleam the guards are made redundant by the type system, and if the type annotations were incorrect then the guards would also be incorrect.

I realize now though that adding guards to the arguments doesn't make any sense, because Gleam is already typechecking those. It could still be valuable to add guards for the return result though, to detect cases where the return value of the function is either incorrectly or insufficiently annotated.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/gleam-lang/gleam/issues/789#issuecomment-691356237, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABOZVBUNOEYRDWD72G7Q5PTSFKY5NANCNFSM4RIPT3EA .

QuinnWilton commented 3 years ago

Sure!

Let's say we're writing an external for maps:find/2. We might do this:

pub external fn find(k, Map(k, v)) -> tuple(_, v) = "maps" "find"

This annotation is incomplete, however, and if the key can't be found in the map, it returns :error. Without any sort of guards on the return value, this :error may end up flowing through the program and lead to a runtime error much later on in the program than where the external was first called.

With the guard, however, the error happens immediately, and it becomes easier to debug that you just have an incomplete annotation, and that you need to handle that error case when reaching into the map.

CrowdHailer commented 3 years ago

This is an interesting idea, and I like the goal. I'm not sure how useful it would in reality. For example.

How do you type the return value of maps.find? If I want to play it safe I would use dynamic as the return type and write my own function to pull it apart e.g.

case dynamic.element(ret, 0) {
  Ok(tag) if tag == ok_atom -> Ok(v)
  _ -> Error(Nil)
}

But if I was to set the return type as dynamic on the external function declaration, then there is no useful guard that can be added.

lpil commented 3 years ago

Another thing is that we inline external functions often, so there's nowhere to add these guards. We could stop inclining them to implement this if we think it's useful, but it does go against the general Gleam pattern of trusting the types and having as little runtime work as possible.

We could have guards for List, Int, Float, String, BitString, and tuple, I think. Maybe single variant records if we're clever.

QuinnWilton commented 3 years ago

Those are great points from both of you. Maybe it's an idea that isn't quite as useful as I thought it would be.

lpil commented 3 years ago

I think there may be something here but I'm not sure what yet. Lets move this to the suggestions tracker.