positron-lang / spec

Positron Language Specification
4 stars 1 forks source link

Functions #13

Closed vinivendra closed 9 years ago

vinivendra commented 9 years ago

A few other issues(#6, #9 and #10) have started to interfere a bit with function definitions and calls, so I think we should try defining these smaller building blocks (such as variable bindings and now functions) before moving on to greater things.

Let's talk about functions!

vinivendra commented 9 years ago

Let me explain why I want this discussion: I think the way most languages handle this is really, really poor.

For instance: in C, whenever I use qsort, I always have to go to the reference pages and look up how the arguments work. As another example, many tutorials that reference more complex API often need to comment their code to explain simple function calls:

glDrawElements(
     GL_TRIANGLES,      // mode
     indices.size(),    // count
     GL_UNSIGNED_INT,   // type
     (void*)0           // element array buffer offset
 );

One could argue that a good IDE could help when using such complicated functions, though I've yet to see one that does. Nevertheless, reading these functions doesn't get any easier, unless you're familiar with the API. I believe we can all think of several examples of big code bases that may use functions such as doSomething(2, 2, 2), when you have no idea what the arguments mean.

Contrast this with an ObjC equivalent:

[array quickSortWithElementSize: 4
               numberOfElements: 10
                compareFunction: myCompare];

[glContext drawElementsWithDrawingMode: GL_TRIANGLES
                      numberOfElements: indices.size()
                           elementType: GL_UNSIGNED_INT
                                offset: (void*)0];

Though I don't like that it's really verbose, I think this code is considerably easy to understand and much more clear. Swift does this too (maybe with the better syntax), but I'm not defending the syntax here, just the principle. What do you think?

igorbonadio commented 9 years ago

I agree that it is considerably easy to understand and much more clear. It was introduced (I think) in smalltalk, which is a beautiful language.

But I think it is too verbose...

I was looking how Swift does this kind of function call, and I think it is better. Ruby 2.0 has something like this too:

def foo(bar: 'default')
  puts bar
end

foo # => 'default'
foo(bar: 'baz') # => 'baz'

But, although I like ruby, I usually don't use this kind o functions...

igorbonadio commented 9 years ago

First, I think Positron shouldn't have exported functions. All functions should belongs to objects (this functions are methods).

So, if you have a very complicated function, maybe it is better to use something like a builder:

Painter.figure(GL_TRIANGLES)
       .numberOfElements(indices.size())
       .eleentType(GL_UNSIGNED_INT)
       .offset(0)
       .draw()
vinivendra commented 9 years ago

I agree on both counts: it's too verbose, and all functions should be methods.

Also, a builder might be better, but maybe we shouldn't assume the user will always use the best programming practice.

Why don't you like ruby's system? I think it might be a nice compromise:

def quicksort(size as natural, count as natural, comparator as (a, a -> int))

array.quicksort(size: 4, count: 10, comparator: myCompare)

Again, I'm on the cellphone. I'm not sure about the definitions, but I like the call syntax. Also, 'as' could easily be ':'.

igorbonadio commented 9 years ago

As I said, it was introduced in Ruby 2.0 and all the standard library doesn't use this syntax. So almost anyone uses it...

vinivendra commented 9 years ago

Oh I get it, that makes a lot of sense. But despite not using it in ruby, do you dislike the syntax itself?

igorbonadio commented 9 years ago

I was thinking... We could accept both, named and unnamed parameters. For example:

def sum(lhs: Int, rhs: Int): Int
  lhs + rhs
end

sum(1, 2) # unnamed parameters
sum(lhs: 1, rhs: 2) # named parameters
sum(rhs: 2, lhs: 1) # named parameters (in any order)
igorbonadio commented 9 years ago

I think it is beautiful... but I don't know if it is practical.

igorbonadio commented 9 years ago

In ruby, people usually use it in the end, for example:

link_to("name", controller: "accounts", action: "signup")
igorbonadio commented 9 years ago

But in this case it is not a named parameter... it is a hash

def link_to(name, opt = {})
  ...
end

link_to("name", controller: "accounts", action: "signup")   # valid
link_to("name", {controller: "accounts", action: "signup"}) # valid
vinivendra commented 9 years ago

I like the idea of allowing both named and unnamed parameters. In a sum method like the one you showed, parameter names really are just clutter. But for standardization's sake, I think maybe the creator of the function should specify which parameters have names and which don't. That way, method calls are always the same (no matter who wrote the code) and are always as clear as the method's creator wanted them to be.

vinivendra commented 9 years ago

I don't really see the point in putting names at the end, but I guess that's a code style choice and it's out of our hands.

I do like the idea of allowing method calls using a hash table (it might allow for some interesting meta-programming solutions), though I'm sure these cases will suffer some performance hits. Still, it would probably be an option worth adding.

igorbonadio commented 9 years ago

I was thinking...

1) Named parameters are in accordance to the 0th law 2) Unnamed parameters are in accordance to the 1st law 3) Using both is not in accordance to the 2nd law

So, if we follow Positron's law, I believe we should use named parameters.

What do you think?

igorbonadio commented 9 years ago

Code examples (imagine that calc is an object that responds to sum):

def sum(lhs: Int, rhs: Int) -> Int
  lhs + rhs
end

x = calc.sum(1, 2)
def sum(lhs: Int, rhs: Int) -> Int
  lhs + rhs
end

x = calc.sum(lhs: 1, rhs: 2)

Here I think we can't use ':' to define the variable's type.

def sum: lhs as Int and: rhs as Int -> Int
  lhs + rhs
end

x = calc sum: 1 and: 2

I realized that we need to use ->. This arrow is the return type. We cant use as (or :) because the type of this function is (Int, Int) -> Int

vinivendra commented 9 years ago

You have a point, one convention would probably be best.

I don't know if I prefer smalltalk/objc's way,

x = calc sum: 1 and: 2

in which the first parameter is "unnamed", or the ruby like syntax:

array.quicksort(size: 4, count: 10, comparator: myCompare)

Maybe it would help if we thought about other syntax details too, like parentheses vs. brackets vs. spaces in function calls:

calc.sum(lhs: 1, rhs: 2)  // vs.
[calc sum: 1 and: 2]      // vs.
calc sum: 1 and: 2

I prefer the first way, but I think if we adopt it we have to force the user to name all parameters.

igorbonadio commented 9 years ago

I prefere the first too. And you, @renatocf , @phrb and @ademar111190 ?

renatocf commented 9 years ago

As a person who has programmed in C, C++, Java and Perl, I really think the first syntax is the clearest. it gives me the idea "call method sum of object calc with 2 parameters". I think that haveing a , as a separator for parameters clearly tell's me when the expression related to initializing a parameter is done. Imagine, for example, that we want to sum two products (method mul):

calc.sum(
  lhs: calc.mul(lhs: 7, rhs: 3),
  rhs: calc.mul(lhs: 3, rhs: 7))

[calc
  sum: calc add: 7 and: 3
  and: calc add: 3 and: 7]

calc
  sum: calc add: 7 and: 3
  and: calc add: 3 and: 7

For me, it's hard to look for the 2nd and 3rd expressions and understand what's going on. And I really can't say that someone that's trying to write 3*7+7*3 with a specialized class has bad programming practives (I believe it's reasonable code). So, I vote for the 1st style.

igorbonadio commented 9 years ago

The second and third expressions need parentheses... without them they are really difficult to understand.

[calc sum: [calc mul: 7 and: 3]
     and: [calc mul: 3 and: 7]]

calc sum: (calc mul: 7 and: 3)
     and: (calc mul: 3 and: 7)
ademar111190 commented 9 years ago

I'm thinking in something like this:

fun sum ← Real(let a : Real, let b : Real) { // let and var can bee applied here
    a.plus(b) //return implicit
}
fun print ← Void(let t : Text) {
}

I liked:

calc sum: (calc mul: 7 and: 3)
     and: (calc mul: 3 and: 7)

but I really prefer:

calc.sum(calc.mul(7, 3), calc.mul(3, 7))

About the 3 styles I prefer the 1st.

phrb commented 9 years ago

From the usability standpoint, I don't like obligatory named parameters.

As visuals go, I think smalltalk style is fugly, and the following call syntax appeal more to me:

Prefix Style:

[calc (sum [calc (mul 7 3)] [calc (mul 3 7)])]

Java/C++ Style:

calc.sum(calc.mul(3, 7), calc.mul(7, 3))
vinivendra commented 9 years ago

@ademar111190, I think enabling vars and lets in function definitions might be really interesting in the way that it allows some parameters to be immutable. I'm afraid it might get a bit too verbose, especially if coupled with named parameters, but it's definitely worth looking into.

@phrb, I get your problems with usability. In a simpler text editor, remembering and writing parameter names may be hard. When done in an IDE, however, it's considerably easier than remembering what each parameter should be, since it can autocomplete all the names for you. ObjC is particularly good in that way.

When we consider this, I think @igorbonadio has a point. Of the styles we have considered so far, it seems like the most attuned to our principles (clarity, if nothing else).

igorbonadio commented 9 years ago

@ademar111190 I think we don't need a var parameter. It's a better style to return values instead of to change the value of a parameter. So I think we should treat all parameter as 'val' / 'let' (that way we don't need to specify it).

@phrb I agree that it is difficult to remember all parameters names... but it is also difficult to remember all parameters order... When I'm programming, I keep the documentation opened. But I think named parameters have a great advantage: when you are reading code, it is easy to know what was written.

igorbonadio commented 9 years ago

I think we agree with the function call syntax:

sum(lhs: 1, rhs: 2)

And what about the method definition?

I like:

# sum: (Int, Int) -> (Int)
def sum(lhs: Int, rhs: Int) -> Int
  lhs + rhs
end

As you can see, I'm assuming that the last expression is the returning value. It will be good when defining annonimous functions (issue #10).

If you want to define a function that returns nothing:

# nothing: () -> ()
# () could be Positron's 'void'
def nothing()
  # something here...
end

We could return multiple values too:

# nothing: (Int, Int) -> (Int, Int)
# I think (Int, Int) should be a tuple
def sum_mul(lhs: Int, rhs: Int) -> (Int, Int)
  (lhs + rhs, lhs * rhs)
end
vinivendra commented 9 years ago

@igorbonadio, do you think than that all parameters should be immutable? That might be interesting.

I like your definition syntax. I agree that it makes anonymous functions easier, I like the tuples and the 'void' syntax.

Initially I was partial to using function instead of def, I generally prefer whole words instead of abbreviations. However, we have already used var before, and I think using three-letter keywords when possible might give a nice look to the overall code (var, let, def, end...).

I interpret end here as an alternative to curly brackets ({ }) and Python-like indentation. I rather like it, I think it makes the code quite clear. This is an important decision though, since it will probably affect ifs, fors, etc.

Kazuo256 commented 9 years ago

Oh yeah, have you guys ruled out Python-like indentation for the language? It enforces an standardization in the code style (which satisfies the second law) and makes the code shorter (which might perhaps satisfy the first law).

renatocf commented 9 years ago

I liked the overall style you purposed so far. Named parameters, as increase clarity in comparison with non-named, is The Right Decision (as we follow Positrion's Laws of Design). And using C/Java/C++ style (as we all agreed) is more simple than other options.

Now, about function definitions: I think @igorbonadio's style is nice. But should no -> mean "return nothing"? In C++11, they included trailing return types. But for simple expressions, they saw it's quite pedantic to write them for small functions (which are very common in OO languages). For example:

def createGoodRobot() -> Robot
  Robot("B2D2")
end

def createBadRobot()
  Robot("Bender")
end

This is a case where we can see (for the "return last parameter" rule) what the return type will be. So, does it worth it to write it twice? I believe it's even more important for lambdas and closures (#10) , as they usually are very short functions.

In this case, I'm not sure how we'd deal with void return types. But if the user had to explicitly say "this function shall not return", wouldn't it be more clear? I mean: I imagine a user writing a function and forgetting to put the trailing return type. Then it returns nothing, but the user expects the last expression to be returned. So, if he had:

def sum(lhs: Int, rhs: Int)
   lhs + rhs
end

var a = sum(lhs, rhs)  # compilation error!

I think that returning nothing is a strong restriction - and one that could give more optimized code (no need to check auto deduced return type consistency). So, perhaps we could have two types of functions:

def sum(lhs: Int, rhs: Int)
   lhs + rhs  # return Int in the last expression
end

def sub(lhs: Int, rhs: Int) -> Int
   lhs - rhs  # return Int in the last expression
end

sub print(lhs: Int)
   print(arg: 42)
end

So, a subroutine is a function that returns nothing. and a function (as the mathematical definition says) always maps a set to another set.

renatocf commented 9 years ago

And the last comment made me though in other issues related to functions:

  1. Will we have generic functions? (e.g. a generic sum function to sum any two types). Does this syntax is compatible with generic definitions?
  2. Will we have operator overloading? Or operators will be just funny names for functions? This influences both the way functions defined and applied.
  3. How will we handle functions with any number of parameters? (e.g. a print or printf function)
vinivendra commented 9 years ago

@renatocf has an excellent point, the example he gave is really ambiguous:

def sum(lhs: Int, rhs: Int)
   lhs + rhs
end

subs are a very interesting way of handling this. The main alternatives I can think of are the use of either -> void or the return keyword, but neither one works well with lambdas. Then again, we will probably use a different syntax to define short lambda functions, even if it's only slightly different. Therefore, the syntax defined here doesn't necessarily have to work with lambdas.

As for the issues: I don't think I really understand 1, but if I do I believe it can be better solved by using 2.

2, to me, boil down to "yes, operator overloading in an excellent idea and I don't get why so few languages do it". In my opinion, we should allow overriding arithmetics, comparisons, even indexing (object[2] and object["foo"]), and maybe a few others we think of along the way. Overriding things like string representation and hash functions might be interesting as well.

As for 3, I'm not a fan of functions with multiple parameters. In print's case, I think it might be better handled by some clever string manipulation in the language, e.g.:

print("Variable myVariable has value \(myVariable)")

I'd also like to raise another issue: 4 - Setters and Getters: ObjC has an interesting policy on this. The syntax object.property is used to access the property directly whenever no getter is implemented, and is used to call the getter whenever it exists. For instance, we could have this in a class definition:

var property: Integer

def getProperty() -> Integer
    // do some stuff
    return property

def setProperty(newValue: Integer)
    // do some stuff
    property = newValue

Since the getter exists, we use it instead of direct access. Same goes for the setter. This also allows for properties that don't take up space because they can be calculated at any time: just implement the setter and the getter and use the same syntax, no need to declare the property itself.

Finally, I think @Kazuo256 has a good point: Python's implementation is really in tune with the 2nd and 1st laws, perhaps we should consider it. Personally, I think both using ends and using indentation are great alternatives to the horrible { }s.

Kazuo256 commented 9 years ago

I believe what @renatocf meant by 1 is something similar to C++'s templates and Java's generics. That is, functions (and classes) that apply to any kind of type. Which may be different from the Any type if the later is something more related to duck-typing. For instance, in C++, a generic function looks like this:

template <typename T>
T genericSum (const T& lhs, const T& rhs) {
  return lhs + rhs;
}

Normally, the real type T represents can only be known at compile-time, and each type used generates a new version of the function or class (like a macro). Unfortunately, I ended up choosing a not so clear example, as it actually depends on 2.

Regarding that topic, one very clean way of supporting operator overloading without creating extra syntax is by using multimethods. With that, methods and operators become the same for both definition and usage. It also helps with 4, because it simplifies making member access to be just another kind of operator. It brings a few other problems though, specially with handling imported modules.

As for 3, I can think of three ways of doing it:

a. Using the stack itself, like with C's varargs or Lua's state stack. It is quite powerful when used together with tail-recursion. b. Making it a syntax sugar for an implicit array parameter. I believe Python does this, right? c. Using something like C++11's variadic templates. But since it is quite complex and confusing (although very fun), Positron's laws would probably reject it.

igorbonadio commented 9 years ago

@Kazuo256 I know that it is very personal, but I don't like python syntax... first because when I need to add a new method at the end of a class, I never know where to put it... I think the end at the end helps. And second because when you want to break lines (for example, when your code is bigger than 80 columns) you have to put a \ at the end of the line.

class Robot:
  def attack():
    for i in range(10):
      print i
# imagine it is a big class, and you can't see its header...

  def bigline():
    obj.method() + obj.method() + obj.method() + obj.method() + obj.method() + \
       obj.method() + obj.method()

  def bigif():
    if ((a > b) and (c < d)) or \
       ((j > k) and (h < m)):
      print "hi"
igorbonadio commented 9 years ago

@renatocf is correct! But I don't like the sub solution...

@vinivendra suggested -> void or something like that. I think I liked it... I have to think more about it... But I don't like to force return. I believe we should have the return keyword, but it should be optional.

igorbonadio commented 9 years ago

@renatocf I believe Positron should have generics (1). But I would like to find a better syntax than C++.

(2) I think Positron shouldn't have operators. For example, + should be a method of Number that can be expressed in the infix order.

(3) I agree with @vinivendra in the case of printf function. String interpolation is really cool! But I think we should have variadic functions. Maybe, as @Kazuo256 said, using a syntax sugar to an implicit array parameter.

igorbonadio commented 9 years ago

@vinivendra raises an interesting question. I liked the getter and setter solution of ObjC. It seems clever. But I think we can do it with metaprogramming instead of a new syntax.

igorbonadio commented 9 years ago

I opened issues to discuss generics (#17) and tuples (#15)

vinivendra commented 9 years ago

Alright, lots of issues here:

1 - Generics went to a separate issue now. 2 - Operator overloading: I think @igorbonadio has the best solution: all operators should just be methods with a different syntax for calls. However, I think the best way to implement this would be with polymorphism:

class Vector
    def multiply(Vector v)
        # inner product
    end

    def multiply(Number a)
        # scalar product
    end
end

3 - Multiple parameters - I agree, implicit arrays would probably be best. 4 - @igorbonadio, how would you propose we do this with metaprogramming?

Kazuo256 commented 9 years ago

4 - @vinivendra With the appropriate metaprogramming features, one can inject new methods at runtime in a class, creating getters and setters after inspecting the relevant fields. Or one may intercept method calls and decide what to do based on the method's name, for instance.

igorbonadio commented 9 years ago

@Kazuo256 @vinivendra we can generate methods at compile-time using Hygienic Macros.

For example, in ruby:

class Robot
  attr_accessor :property
end

And we can use @vinivendra 's idea of customizing getter and setter passing a block to this macro, for example.

vinivendra commented 9 years ago

Alright, that looks good.

I was talking to @renatocf, and we like the idea of using metaprogramming for this. To us, the language should be compact, but allow for significant changes in its structure; some of these changes would be included in a standard library, meant to be used by most programmers - except the ones that want to experiment with the language itself and change it significantly.

Based on that, we separated what we think should be on the language and what could be on the library.

class Circle
    var radius: Real

    def radius() -> Real # getter
        return radius
    end

    def setRadius(newValue: Real) # setter
        radius = newValue
    end

    def area() -> Real # another getter, though there's no area property
        pi * (self.radius ^2)
    end

    def method()
        radius = 3 # direct access

        self.radius = 3     # implicit call to the setter
        let r = self.radius # implicit call to the getter
        let a = self.area   # implicit call to the getter 
    end
end
class Circle
    var radius: Real
    var area: Real (get)

    def area() -> Real
        return pi * (self.radius ^2)
    end

    def method()
        radius = 3 # direct access
        self.radius = 3     # implicit call to the setter
        let r = self.radius # implicit call to the getter

        let a = self.area   # implicit call to the getter 
        area = 20 # direct access, ok because the property exists

    end    
end

I think @renatocf agrees with me on the rules I stated, though we both think individual syntaxes have room for improvement.

igorbonadio commented 9 years ago

Ok. We can discuss it. Lets open another issue? We could open one to discuss "operator", and metaprogramming.

But I think we should define the syntax of function definition first.

My proposal is:

def sum(lhs: Int, rhs: Int) -> Int
  lhs + rhs
end

def print(text: String) -> ()
  # ...
end

and function call:

sum(lhs: 1, rhs: 2) # => 3
print("something") # => ()
renatocf commented 9 years ago

I liked this proposal, in place of a void keyword. But woudn't be better to make functions to be declared like this?

def sum(lhs: Int, rhs: Int) -> (Int)
  lhs + rhs
end

def print(text: String) -> ()
  # ...
end

I feel it's more standardized. Every function maps from a list of parameters to another list of parameters - which can be empty in both cases.

renatocf commented 9 years ago

@igorbonadio. I believe we need some type of control over the number of Issues we're opening. I know ideas bring ideas, but it's hard to comment and follow all of them. So, I suggest we could do an administrative issue "Proposed Features", in which we could list this ideas as we come up with them. And we should not open new features (or keep them opened) if we pass a maximum number (I suggest 5). This will force us to include decisions in the official spec or to close a discussion while we discuss another one (e.g. close Anonymous Functions to discuss general Functions).

igorbonadio commented 9 years ago

I liked @renatocf 's proposal. It is a lot more standardized.

igorbonadio commented 9 years ago

@renatocf I agree with you. It will make us more productive too. At this moment we have 9 issues opened. So lets close them first.

I think 5 is a good number.

igorbonadio commented 9 years ago

Functions

function : function_header function_body 'end'
         ;
function_header : 'def' ID '(' params? ')' '->' '(' returning_types? ')'
                ;

function_body : expression*
              ;

params : (ID ':' ID ',')* ID ':' ID
       ;

returning_types : (ID ',')* ID
                ;
def sum(lhs: Int, rhs: Int) -> (Int)
  lhs + rhs
end

def print(text: String) -> ()
  # ...
end

What do you think?

vinivendra commented 9 years ago

Personally, I'd prefer not using parentheses for the return values - I think it's more clean, and in general Id like to avoid using special characters to enhance readability.

However, I agree that using parentheses is a lot more standardized (it might even be an incentive to using functions with multiple return values), so I'm ok with this syntax.

renatocf commented 9 years ago

Marking as opened to #18

renatocf commented 9 years ago

At first, it's also not the most beautiful thing for me, @vinivendra. But it has the qualities you commented. I see this declaration as "a function maps a tuple of types to another tuple of types". And, as every function parameter is constant, it does not change the environment around it. What I think is great, since it's a source of problems in typical procedural/OO languages.

vinivendra commented 9 years ago

What do you mean by "every function parameter is constant"? Are they immutable?

Also, something just occurred to me. Isn't it possible that multiple return values will have the same problems as unnamed parameters had? As a random example, shouldn't we perhaps have:

# Instead of
def fetchRequestsFromURL(URL: String, mode: Integer) -> (Bool, String)
    # do some stuff
    return (true, "hello world")
end

# we do
def fetchRequestsFromURL(URL: String, mode: Integer) -> (success: Bool, results: String)
    # do some stuff
    success = true
    results = "hello world"
    return
    # or
    return (true, "hello world")
end

On the one hand, this won't change much when reading code, since it's likely we'll have function calls like success, results = fetchRequestsFromURL(...). On the other, this helps when writing code, since you know exactly what the return values mean. In the first example, one would definitely have to read the documentation, which is something we're trying to avoid.

This example got a bit too verbose for my taste, but I just wanted to get the general idea across.

vinivendra commented 9 years ago

Also, someone had the idea of using default values in functions, and therefore allowing optional parameters. What do you guys think?

Inspired by Python:

def sort(comparator: Comparator, mode: String = "quicksort")
    #...
end

array.sort(comparator: myCompare) # ok, mode defaults to "quicksort"
array.sort(comparator: myCompare, mode: "mergesort") # ok too
array.sort(mode: "mergesort") # error: no default value for the 'comparator' parameter
renatocf commented 9 years ago

@vinivendra, I though parameters should be immutable, as this would stimulate users to return values instead of modifying the parameter's ones. But I think it's not possible, or every time users want to change an object, they'd have to copy and change the copy. And this affects performance...