positron-lang / spec

Positron Language Specification
4 stars 1 forks source link

Anonymous Functions #10

Open igorbonadio opened 9 years ago

igorbonadio commented 9 years ago

As we are thinking that positron should have some functional features, I believe we should discuss anonymous functions.

igorbonadio commented 9 years ago

My first suggestion:

let sum = [a: Int, b: Int | a + b]

But there is a problem here... How can I explicitly declare the returning type?

Maybe:

let sum = [a: Int, b: Int | a + b] : Int

Or

let sum : (int, Int) -> (Int) = [a, b | a + b]

where (Int, Int) -> (Int) is the function type.

igorbonadio commented 9 years ago

And function call could be

sum(1, 2)                              # => 3
# or
[a: Int, b: Int | a + b](1, 2)         # => 3
# or
([a: Int, b: Int | a + b] : Int)(1, 2) # => 3
igorbonadio commented 9 years ago

Changing : to ->

let sum = [a: Int, b: Int | a + b] -> Int

Or

let sum : (Int, Int) -> (Int) = [a, b | a + b]

where (Int, Int) -> (Int) is the function type.

igorbonadio commented 9 years ago

Updating function syntax:

let sum = [lhs: Int, rhs: Int | lhs + rhs] -> (Int)

Or

let sum : (Int, Int) -> (Int) = [lhs, rhs | lhs + rhs]
igorbonadio commented 9 years ago

I was think how it will work in map-like functions

vector.map([element: Int | element*element] -> (Int))
vector.map([element | element*element])

And there is a problem here... how can this kind of function be generic... remember, we use named parameters...

class IntVector
  def map(function: (Int) -> (Int))
    ...
    function(e) # error... 
    ...
  end
end
renatocf commented 9 years ago

Marking as opened to #18

igorbonadio commented 9 years ago

As we agreed in issue #13:

class IntVector
  def map(function: (Int) -> (Int)) -> (IntVector)
    ...
    function.call(parameters: (e)) # ok!
    ...
  end
end
igorbonadio commented 9 years ago

I was thinking... Do you like ruby-like block syntax? We could use it here:

vector.map do |element|
  element*element
end

# or

vector.map { |element| element * element } # but I think we should cut this form.

And to define new anonymous functions, it can be done building a new object:

Proc.new |element|
  element*element
end

I will think if we can use something like this.

igorbonadio commented 9 years ago

What do you think?

# vector is a vector of integers

vector.map do |element: Int| -> (Int)
  element*element
end

vector.map do |element|
  element*element
end

# it is just a sugar syntax

# an anonymous functions is

Function.new do |element: Int| -> (Int)
  element*element
end

# so you can do

f = Function.new do |element: Int| -> (Int)
  element*element
end

vector.map(function: f)

# the rule is: If the function is the last 
# parameter, you can use the sugar syntax
igorbonadio commented 9 years ago

As we decided how to define functions (#13), now we need to decide how to define anonymous functions.

I was thinking in something like:

var sum(lhs, rhs) = [ (lhs: Int, rhs: Int -> result: Int) |  result = lhs + rhs]
var sum(lhs, rhs) : (Int, Int) -> (int) = [ (lhs, rhs) -> (result) | result = lhs + rhs]

Both are valid. The first one the type is deduced. The second one the type is explicitly defined.

What do you think?

vinivendra commented 9 years ago

I like your idea, but I think we can simplify it a bit. There are three elements here, right? The name, the type and the body.

# either 
var myFunction(first, second -> third, fourth)
# or
var myFunction(first, second) -> (third, fourth)

I'm cool either way, but I'll use the second one for these examples.

var sum(lhs, rhs) -> (result): (Integer, Integer) -> (Integer) # ok
var sum(lhs, rhs) -> (result) = [...] as (Integer, Integer) -> (Integer) # we don't allow this, right?
#or
def method(function: (Integer, Integer) -> (Integer)) -> ()
    #...
end

method([...]) # we know what the method expects, so we know what the function's type is.
# if the function's name tells us what the variables are called:
var sum(lhs, rhs) -> (result): (Integer, Integer) -> (Integer) = [result = lhs + rhs]
# If there's no function name, we have to write it down:
method([(lhs, rhs) -> (result) | result = lhs + rhs])

Otherwise, I'm afraid the first way will start getting too verbose (and repetitive) for a simple sum function. It's already too long for my taste, but I don't think we can shrink it that much more.

vinivendra commented 9 years ago

This way, actually, anonymous functions would look a lot like normal functions, which is something I like. If we needed more than one statement, we might be able to do it by simply using multiple lines:

var addAndMultiply(lhs, rhs) -> (sum, product): (Integer, Integer) -> (Integer, Integer) =
    [ sum = lhs + rhs
      product = lhs + rhs ]

or maybe commas:

var addAndMultiply(lhs, rhs) -> (sum, product): (Integer, Integer) -> (Integer, Integer) = [ sum = lhs + rhs, product = lhs + rhs ]

I still think maybe this is too long, but I guess we can always separate the statement in multiple lines to make it more readable.

// same as above, but with an alternative indentation
var addAndMultiply(lhs, rhs) -> (sum, product):
                  (Integer, Integer) -> (Integer, Integer) =
                  [ sum = lhs + rhs
                    product = lhs + rhs ]

var addAndMultiply(lhs, rhs) -> (sum, product):
                  (Integer, Integer) -> (Integer, Integer) =
                  [ sum = lhs + rhs, product = lhs + rhs ]

Personally, I prefer the first way.

igorbonadio commented 9 years ago

I agree. But I prefere the first one (without commas):

var addAndMultiply(lhs, rhs) -> (sum, product):
                  (Integer, Integer) -> (Integer, Integer) =
                  [ sum = lhs + rhs
                    product = lhs + rhs ]

I seems good in high order functions too:

def map(f(a) -> (b): (Integer) -> (Integer), vector: [Integer])
  // ...
end

map([b = a*a], [1, 2, 3, 4, 5, 6]) // => [1, 4, 9, 16, 25, 36]
vinivendra commented 9 years ago

Sounds good to me. Can we close the issue?

igorbonadio commented 9 years ago

I think we can close it.

renatocf commented 9 years ago

Just a question: can't we define a simpler way of deduce what will be the types of a lambda? I think that one of the greatest advantages of having lambdas is to simplify the definition of functions when passed as parameters. So, in @vinivendra example:

def map(f(a) -> (b): (Integer) -> (Integer), vector: [Integer])
  // ...
end

map([b = a*a], [1, 2, 3, 4, 5, 6]) // => [1, 4, 9, 16, 25, 36]

How do we know [b = a*a] has type (Integer) -> (Integer)? And will we check if the function really uses parameter a and sets return b in a function application (as with map)? As far as I know, the name f(a) -> (b) is to be used inside the method - and an external function could have any names, since it had the same type. So, using "normal" functions:

def square(number: Integer) -> (square: Integer)
  square = number * number
end

def map(f(a) -> (b): (Integer) -> (Integer), vector: [Integer])
  // ...
end

map(square(number) -> (square), [1, 2, 3, 4, 5, 6]) // => [1, 4, 9, 16, 25, 36]

I think we have to think better about this things before closing this issue.

igorbonadio commented 9 years ago

Yes! You are right @renatocf . We have to discuss it. But I think it works. Let me explain:

1) How do we know [b = a*a] has type (Integer) -> (Integer)?

def map(f(a) -> (b): (Integer) -> (Integer), vector: [Integer])
  # ...
end

As you can see, map define that f should be of type (Integer) -> (Integer). So we can infer it at compile-time.

(scala and haskell do something like this)

2) How can we know the names of the parameters?

Check the following code:

var f(a) -> (b) : (Integer) -> (Integer)
f(a) -> (b) = [b = a * a]

Do you agree that it works? The compiler can infer the type and the name of the parameters. So it can be done in the map code.

3) How does it work with "normal" functions

Consider the following code:

def square(number: Integer) -> (resp: Integer)
  resp = number * number
end

var f(a) -> (b) : (Integer) -> (Integer)
f(a) -> (b) = square

Do you agree that it works too? In this case, a = number and b = resp.

So, again, map works with "normal" functions.

igorbonadio commented 9 years ago

Ah... I forgot:

map(f(a) -> (b): [b = a*a], vector: [1, 2, 3, 4, 5, 6]) // => [1, 4, 9, 16, 25, 36]
igorbonadio commented 9 years ago

I think there is one thing missing here: How can we call an anonymous function expression?

[sum = a + b](a: 1, b: 2) # => 3

I can see how the compiler can infer the type of a, b and the return value. It is easy to see the name of the input parameters... but where is the name of the return value?

Kazuo256 commented 9 years ago

Wouldn't it be something like

var x: Int;

(sum: x) = [sum = a + b](a: 1, b: 2);

?

igorbonadio commented 9 years ago

hummmm... And what about the following example:

def mul(lhs: Int, rhs: Int) -> (result: Int)
  result = lhs * rhs
end

mul(lhs: 2, rhs: [rhs = a + b](a: 1, b: 2)) #= > 6

Is it correct?

Kazuo256 commented 9 years ago

It is correct because, if I remember correctly, when a function has only one return value, its name may be omitted. If there were more return values, I wouldn't know how to pass them along as parameters to another function, be it an anonimous function or not.

igorbonadio commented 9 years ago

Yes, you are correct

var x = sum(a:1, b: 2) # is valid. We don't need to name single return values

But the problem is the name of the return variable... Will we infer it as rhs because of the name of the parameter of mul?

I think it is great.

Does everyone agrees with it? If so, I think we can close this one too.

igorbonadio commented 9 years ago

Ah, we need a proposal...

Kazuo256 commented 9 years ago

So, if you wanted to get more than one return value from a lambda, it would like this?

def mul(lhs: Int, rhs: Int) -> (result: Int)
  result = lhs * rhs
end

mul([lhs = x+x, rhs = x+2](x: 10))

Yeah, looks like it works.

vinivendra commented 9 years ago

Actually, I think that's not how it would go. Functions that return more than one value actually return a tuple, which we may or may not unpack, right?

def randomInts() -> (one: Integer, two: Integer)
    one = 1
    two = 2
end

var tuple: (Integer, Integer) = randomInts() # receiving the tuple
var x, y
one: x, two: y = randomInts() # receiving the tuple and unpacking it

In @Kazuo256's case, the function returns a tuple of two Integers. Because of that, the mul method would have to receive a tuple of two Integers instead of receiving two Integers explicitly, since we decided to disallow automatic unpacking of values.

vinivendra commented 9 years ago

Your proposal then is to allow for anonymous functions to be valid

(sum: x) = [sum = a + b](a: 1, b: 2);

Is that it?

igorbonadio commented 9 years ago

Yes. I agree with @vinivendra . @Kazuo256 's example should not be valid. First because there is no parameter name, and second because the function returns a tuple.

@vinivendra , check if I understood what you said:

var double(a) -> (b) : (Integer) -> (Integer) = [b = 2*a]
mul(lhs: 2, rhs: [rhs = a + b](a: 1, b: 2))
(sum: x) = [sum = a + b](a: 1, b: 2);
vinivendra commented 9 years ago

Kind of, I think your second example actually falls into the third category, since we only know the parameter names because we're calling the function immediately. I was thinking something like this:

def apply(function(a)->(b): (Integer)->(Integer)) -> (result: Integer) 
    result = function(a:2)
end

apply(function(a)->(b):[b = 2*a]) # => 4

When we call apply, we know it expects a function with a parameter named a and a return value named b, so we use those names in our lambda. It's a lot like the first example.

igorbonadio commented 9 years ago

Yes. Ok. It is similar to the first.

So, did we reach a conclusion?

vinivendra commented 9 years ago

Yeah, I'm ok with this.