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

Fails to compile when applying the result of a function #56

Closed
lepoetemaudit opened this issue Dec 1, 2016 · 7 comments
Closed

Fails to compile when applying the result of a function #56

lepoetemaudit opened this issue Dec 1, 2016 · 7 comments
Assignees

Comments

@lepoetemaudit
Copy link
Contributor

When a function returns a function, it cannot be applied (in MLFE - from Erlang, the resulting function can be called).

module curry_fun

export curried/1

curried arg1 =
  let curried2 arg2 =
    let curried3 arg3 = 
      arg1 + arg2 + arg3
    in curried3  
  in
    curried2

From Erlang, it works fine:

2> (((curry_fun:curried(1))(2))(3)).
6

But in MLFE, this won't compile: curried 1 2 3 resulting in a compiler error:

mlfe_typer:type_modules/2 crashed with error:{error,
                                              {arity_error,curry_fun,11}}

Using parens (to prevent what I presume is greedy application above) produces a different error:

v = ((curried 1) 2) 3

-------

mlfe_typer:type_modules/2 crashed with error:function_clause

@danabr
Copy link
Contributor

danabr commented Dec 7, 2016

I think the first arity error makes sense. What if there was also a curried/2 and curried/3? In both Erlang and MLFE functions with the same name but different arity are considered different functions. The call curried 1 2 3 is thus ambiguous.

I don't know why the parenthesized version does not work. Spelling it out works:

test_curried () =
  let f1 = (curried 1) in
  let f2 = f1 2 in
  f2 3

The exact error ((curried 1) 2) 3 fails with is:

   {error,
    {invalid_fun_application,
     {error,
      {invalid_fun_application,
       {mlfe_apply,undefined,{symbol,13,"curried"},[{int,13,1}]},
       [{int,13,2}]}},
     [{int,13,3}]}}],

@lepoetemaudit
Copy link
Contributor Author

Indeed, I'd mistakenly thought that only one function (regardless of arity) was allowed per name per module, therefore auto-currying would be possible, but I see that this was an oversight that is being corrected.

I agree with @danabr that multiple arity functions and auto-currying do not mix well. Scala has a special syntax for curried functions: http://docs.scala-lang.org/tutorials/tour/currying.html but I think that would be unnecessarily complicated. I guess my concern is that people familiar with other ML languages may be surprised by the lack of automatic currying, and things like point-free style are difficult to implement without it.

I suppose the typer could allow automatic currying if one and precisely one variant only of a function were defined, and throw an appropriate error in the case that currying would lead to ambiguous use. I guess the sweet spot is how to be a nice Erlang citizen while being as nice an ML as possible :)

The problem with not being able to apply the result of functions stems from here:

https://github.com/alpaca-lang/alpaca/blob/master/src/alpaca_parser.yrl#L404

As far as I understand it there's no rule to match applying on a general expression (specifically here, an apply expression). I wrote a test for the case and managed to write a rule in the parser, but I think that the alpaca_apply record will need an extra field that can take a general expression as the function instead of just Name as it has it currently. (theoretically this would also allow anonymous functions in future).

@j14159
Copy link
Collaborator

j14159 commented Dec 11, 2016

I'm completely open to a discussion about multiple arity versions vs automatic currying, I just stuck that "fix" in today because it felt a little closer to Erlang and I was writing some Alpaca that could take advantage of it :) If it makes sense to back that out in favour of auto-currying I don't really have a problem with it tbh. Would be curious to hear arguments for/against.

As for the AST changes I think we're going to end up wrapping the returns in core-erlang let-bindings in the code generation stage as the following:

-module(test).

curried(X) ->
    fun(Y) -> X + Y end.

apply_curried() ->
    (curried(2))(3).

becomes this in core Erlang (some output redacted for brevity):

'curried'/1 =
    %% Line 3
    fun (_cor0) ->
        %% Line 4
	( fun (_cor1) ->
          call 'erlang':'+'
    	  (_cor0, _cor1)
	  -| [{'id',{0,0,'-curried/1-fun-0-'}}] )
'apply_curried'/0 =
    %% Line 6
    fun () ->
    let <_cor0> =
    	%% Line 7
        apply 'curried'/1
            (2)
        in  %% Line 7
            apply _cor0
                (3)

In this case should we have a different AST node for application of expressions or replace name with the expectation of a name or an expression? Maybe the latter is best, it's going to result in a branch in the code generation step regardless.

@lepoetemaudit
Copy link
Contributor Author

I like the latter approach - the AST node would have a 'function' part which is either a name of a function or an expression which yields a function.

I'll present my argument here for my preferences on autocurrying.

I'm very much in favour of autocurrying. SML, Miranda, Haskell, Elm and OCaml all implement it, and it makes the language very malleable. I propose we allow autocurrying and multiple arity functions, but if more than one variant of a function is defined and not enough arguments are provided to match any variant, the typer errors with an error message explaining that autocurrying is disallowed in these circumstances (and suggest renaming the variants).

If push came to shove, I'd personally prefer autocurrying to multiple arity. I don't think the loss is too great - you just have to give functions different names - but you gain a great deal. One example is that a function like |> otherwise only works on functions which take one argument but with autocurrying it works with more, as in:

-- Imaginary function filter == (a -> bool) -> list a -> list a
let filtered = 
  [1, 2, 2, 3, 3, 3] |> filter ((==) 2)

-- result: filtered == [2, 2]

...so this imaginary filter function takes a predicate function that returns a bool, a list of 'a and returns a list of 'a. We call filter with only one argument, so we get a function that is 'waiting' for a list. |> applies the value on the left to the function on the right, which is now the curried form that only takes one argument, and everything works out :) I snook another example in there too - ((==) 2) being a prefix invocation of ==, meaning we get a curried function that takes a single argument that will return true if the argument is 2. The level of expressivity you can achieve with a really small toolbox is incredible, and people coming from Elm, OCaml etc., will feel right at home with it.

Counter-arguments for me are - we're in Erlang land and multiple arity is a natural fit there. Also we can emulate autocurrying above by providing a standard library with 'pre-curried' functions with different arities and functions for currying, and at worst we can always return / wrap using anonymous functions, once those are implemented. It's also possible to get carried away with techniques like point free style which can lead to terse and hard to read code.

@j14159
Copy link
Collaborator

j14159 commented Dec 13, 2016

I'm quite reluctant to remove differing arity definitions for the same named function since it's common enough in the Erlang world that removing it might be a stumbling block for those coming from there but autocurrying makes enough sense to make it happen. My primary concern is bounding ambiguity - your proposal to simply drop out with a type error when we don't have enough information to make a decision seems pretty reasonable to me provided we make things clear enough for a user.

So the following would generate an error along the lines of {error, {ambiguous_application, foo/1, foo/2}} - but maybe less hostile than that :)

foo x = x + x

foo x y = x + y

make_an_error () = 1 |> foo

While the following would not:

foo x = x + x

foo x y z = x + y + z

-- unit -> (int -> int)
passes_typing () = 2 |> foo 1

Thoughts? Am I clinging too tightly to the differing arity thing?

@lepoetemaudit
Copy link
Contributor Author

I think that looks great. I'm sure if it ever does get confusing there will be ways of disambiguating, syntactically if needs be.

@j14159
Copy link
Collaborator

j14159 commented Dec 14, 2016

I've opened issue #74 to address the automatic currying piece of this so we can fix the basic expression application problem on this one.

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

No branches or pull requests

3 participants