Closed vinivendra closed 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?
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...
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()
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 ':'.
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...
Oh I get it, that makes a lot of sense. But despite not using it in ruby, do you dislike the syntax itself?
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)
I think it is beautiful... but I don't know if it is practical.
In ruby, people usually use it in the end, for example:
link_to("name", controller: "accounts", action: "signup")
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
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.
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.
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?
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
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.
I prefere the first too. And you, @renatocf , @phrb and @ademar111190 ?
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.
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)
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.
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))
@ademar111190, I think enabling var
s and let
s 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).
@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.
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
@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 if
s, for
s, etc.
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).
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.
And the last comment made me though in other issues related to functions:
sum
function to sum any two types). Does this syntax is compatible with generic definitions?print
or printf
function)@renatocf has an excellent point, the example he gave is really ambiguous:
def sum(lhs: Int, rhs: Int)
lhs + rhs
end
sub
s 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 end
s and using indentation are great alternatives to the horrible { }
s.
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.
@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"
@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.
@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.
@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.
I opened issues to discuss generics (#17) and tuples (#15)
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?
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.
@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.
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.
object.property = value
always calls the setter. If there's no setter, the compiler throws an error.object.property
always calls the getter. If there's no getter, the compiler throws an error.set
, have at least one argument and no return value can be used with the setter syntax.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
var variable: Type
syntax declares not only the property but also default setters and getters. Setters and getters implemented by the user automatically override the default ones.var variable: Type (get)
syntax creates the property and a default getter. Any attempts to create a setter result in an error - this is a read-only variable.var variable: Type (set)
syntax does the same, but with setters.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.
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") # => ()
return
keyword to return a value and exit the functionI 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.
@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).
I liked @renatocf 's proposal. It is a lot more standardized.
@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.
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?
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.
Marking as opened to #18
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.
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.
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
@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...
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!