#325 Functions as first class values ... not realy first class

helium Wed 30 Jul 2008

I just wanted to fold a simple function over a list, but I got strange error message that I did not realy understand.

So I tried something very simple and minimal: folding a self written add-function over a list and ... it did not work. My first try looked something like this:

static Int add(Int x, Int y)
{
    return x + y
}

...
result := [5, 4, 3].reduce(0, add)

Results in

Invalid args add(sys::Int, sys::Int), not ()
Invalid args reduce(sys::Obj, |sys::Obj,sys::Int,sys::Int->sys::Obj|), not (sys::Int, sys::Int)

OK, ... after playing around a bit I came to this ugly hack:

result := [5, 4, 3].reduce(0, &add())

Which results in

Invalid args reduce(sys::Obj, |sys::Obj,sys::Int,sys::Int->sys::Obj|), not (sys::Int, |sys::Int,sys::Int->sys::Int|)

OK, what is this third parameter? (The doc's don't tell, but I later found out that it's a counter.) Anyway you can leave arguments out if you don't need them so this should not be the problem. The problem is the first argument of the add function which needs to be Obj ... hmmm, that is a serve problem of reduce. So I have to change my add function to

static Int add(Obj x, Int y)
{
    return (Int)x + y
}

to make it work. But that's obviously stupid and not what you want, if you want to use add elsewhere. So an alternative is to use the normal add and to define a wrapping lambda function:

result := [5, 4, 3].reduce(0) |Obj x, Int y -> Int| { return add(x, y) }

But that's ugly and it's not DRY if you have to do this more than once. So you could define a wrapper function around add so you can use add normally and use this wrapper function when you want to fold it. But that's not very satisfying. And you better define this wrapper function as a lambda like this

addWrapper := |Obj x, Int y -> Int| 
{ 
    return add(x, y)
}

rather than as a named function like

Int addWrapper (Obj x, Int y)
{ 
    return add(x, y)
}

because than you don't have to use this strange &addWrapper() hack, but you can't do that on class level :(

So what am I missing?

brian Wed 30 Jul 2008

The problem here isn't that functions aren't first class, it is how Fan's type system treats functions. If we define a method which takes |Obj|, then there is no guarantee that a function which requires an Int such as |Int| is going to work. Which is why the compiler is complaining - it is a static typing problem (which I agree happens to be extremely annoying for reduce).

However we have now solved this problem with the implicit casting feature (which isn't yet in the released build). Tonight I will verify that implicit casting solves this problem and allows you do just do this:

result := [5, 4, 3].reduce(0, &add)

brian Wed 30 Jul 2008

I checked this case against the latest code, and it is better but still not correct.

Function type compatibility is different here, so what I need to do is sit down and define the formal rules for how implicit casting works for function types and update then compiler.

I'm on vacation next week, so I probably won't get to that for two weeks.

brian Tue 4 Nov 2008

The original definition of implicit casting was not working correctly for function types. I've updated the compiler so that it auto-casts function types. If it knows that function types are incompatible at compile time it will still a compiler error. But if the function types are potentially compatible then an implicit cast is generated.

So Helium's example code should work intuitively now:

class SomeClass
{
  static Int addMethod(Int x, Int y) { return x + y }
  static Void main()
  {
    list := [5, 4, 3]
    addFunc := |Int x, Int y->Int| { return x + y }
    a := list.reduce(0, &addMethod)
    b := list.reduce(0, addFunc)
    c := list.reduce(0) |Int x, Int y->Int| { return x + y }
    echo("$a, $b, $c")
  }
}

In this case reduce is expecting a function of |Obj, Int|, but we are passing a function typed as |Int, Int|. So according to the type system there is no guarantee that reduce will call the function with two Ints. However since Obj could potentially be an Int we allow the call with an implicit cast.

However if we tried to do something like the following it would be a compile time error since we know the second Int parameter can never be a Str:

d := list.reduce(0) |Int x, Str y->Int| { return 0 }

Login or Signup to reply.