4 ms·
I understand the author's position, and despite its usefulness, I think the numbered arguments are an inelegant solution. In Haskell, by way of comparison, any
by hackyhacky 3y ago
I understand the author's position, and despite its usefulness, I think the numbered arguments are an inelegant solution. In Haskell, by way of comparison, any one-parameter function can be used as the argument to map:
ghci> map reverse ["hello", "goodbye"]
["olleh","eybdoog"]
is (mostly) equivalent to the more verbose form with an explicit lambda:
ghci> map (\s -> reverse s) ["hello", "goodbye"]
["olleh","eybdoog"]
Operators are just normal functions, but tuples are not automatically uncurried. Here I'm using the multiply operator (*) to multiple a list of pairs:
ghci> map (*) [(1,2), (3,4)]
ERROR
ghci> map (uncurry (*)) [(1,2), (3,4)]
[2,12]
Points-free syntax can be used to construct chains of operations without any explicit lambda variable. Here filter strings of more than two characters:
ghci> filter ((>2) . length) ["a","bb","ccc","dddd"]
["ccc","dddd"]
In lambda syntax:
ghci> filter (\x -> (length x) > 2) ["a","bb","ccc","dddd"]
["ccc","dddd"]
I find the Haskell syntax concise and clear, and I always miss it when using other languages (despite Haskell's other shortcomings).
Edit: formatting
- jerf 3y agoCurrying is fun and I miss it sometimes as well, but as near as I can tell, it is essentially immiscible with traditional languages. I've seen so, so many attempts, and they are so, so bad. Surprisingly few attempts even seem to get over the bar of understanding what currying is, and why it isn't just partial application. Now, I am not too concerned with which is "better" or anything, nor do I much care for pedantry for the sake of pedantry, but in this case it's a matter of type signatures; curry(some_function) and partial_apply(some_function, param1, param2) are fundamentally different type signatures and for that reason, not because of abstract verbal purity or something, aren't the same thing. I mean, right out of the gate you have the problem that in most conventional languages you're looking at your first example being map(reverse)(["hello", "goodbye"]). Whether Ruby syntax could be bent into making it work without parens (or Perl perhaps) I don't know but with all the other things going on in those syntaxes I'm sure it would be quirky as can be, possibly quirky beyond practical usefulness. And in dynamic scripting languages you'd be looking at a lot of actual function calls, so performance is going to be trashed if you use this pervasively.
- hackyhacky 3y agoI agree that the Haskell approach is not easily portable to other languages. At least according to the article, the Ruby community is struggling to find a solution. I don't think parentheses (or their absence) is a decided factor: for example, I don't find the hypothetical Ruby syntax ["hello","goodbye"].map(&reverse) to be offensive or wildly inconsistent. Note that the map function itself doesn't need partial application, only its function parameter. For the same reason, I disagree that performance in dynamic scripting languages would suffer unduly.
- zverok 3y ago> I don't find the hypothetical Ruby syntax ["hello","goodbye"].map(&reverse) to be offensive or wildly inconsistent. This syntax wouldn't work in Ruby, because bare `reverse` is already a method call, not a reference to a method by name. Allowing method calls without parentheses is crucial to Ruby's design, where all objects are fully opaque, and every `obj.attr` looking like a getter, is just a call to an instance method `attr`. (This is, as far as I understand, the opposite to similar languages like Python/JS, where the object is a dictionary of attributes, and `obj.method` reference the attribute of type "method", while `obj.method()` is an invokation.) This design became a huge drawback in the age of functional(ish) programming, because the simplest way for refer to a method in Ruby is `method(:its_name)` (which is also inefficient, because it creates wrapping object of class Method on the fly, there is no pre-existing first-class object), and any attempt of passing/combining methods would be cumbersome due to it. FWIW, you can do that in Ruby: (JSON.method(:parse) >> method(:puts)).call("[1, 2, 3]") ...which is semantically cool, but looks ridiculous. Another important trait of Ruby's design is that most of the important methods belong to their first argument, so it is not `reverse(string)` but `string.reverse`, so even to obtain a reference to a method, you need to have an object it belongs to! So you can't do that: method(:>).call(a, b) ...because all operators are called on their first operands, so you need this this: a.method(:>).call(b) ...which will require to refer to at least a first argument of the operator, so you can't produce "argument-less comparison operation to call later".
- jerf 3y ago
- rapind 3y agoJust my opinion here... The first example with map reverse is very readable and an improvement over lambda syntax. The chaining example (filter on length) is horrible for readability and I greatly prefer the lambda syntax.
- vidarh 3y ago"Just passing a function" is incompatible with Ruby's current syntax where a bare method name calls the method unless you then introduce a way for method definitions to defer evaluation of its arguments, but then you open a can of worms given that Ruby allows aliasing and replacing arbitrary methods of already defined classes. I suspect it'd be a nightmare of inconsistencies.
- graypegg 3y agoIt’s a bit pedantic to mention, but there is #method(:method_symbol), and obviously &:method_symbol for some cases. Both of which give you a proc you could call later with arguments provided to it. It’s not “the function” since, it’s really an anonymous function, bound to something, that will send :method_symbol to that thing. Gets around the bare-method-runs-method and metaprogramming issues with current Ruby syntax.
- vidarh 3y agoThe entire point was that those constructs that do not just pass the bare method or something like a Proc that allows you to call the method, are needed exactly because the syntax will not allow for just mentioning a bare method name without calling it. If you're fine with the extra syntax, yes, there are many alternatives, but they don't address the point.
- zverok 3y agoI made a small nod towards Haskell's way of doing things in the last paragraph of "How others do it" section: > We might also start look into the concept of the “tacit programming”, which, from some point of view, is also about “not repeating the arguments,” but this would be a much longer post—while it is obscenely long already. Note though that my analysis was mostly about "how you would do that in an established language," not "how would I design a language from scratch." And Ruby is very different from Haskell in its syntax, semantics, and typical intuitions of the language writer and reader. So designing a solution for clearer representation of "default parameters" is quite different in a language which is built around an `object.method(argument) { block }` as its atomic phrase, and one built around `function(arguments)`. (This can turn into a discussion of which phrase structure is more elegant in general... But I believe it was already done quite a few times in a history of programming languages evolution!)
- drewbug01 3y agoRuby does allow you to pass a one-parameter function as the argument to `map`: irb(main):001:0> ["foo", "bar"].map(&:reverse) => ["oof", "rab"] Not precisely the same as what you've outlined, of course, but perhaps useful nonetheless.
- inopinatus 3y agoFun fact, the arity of functions generated by Symbol#to_proc is not 1 but -2 (that is, additional arguments are passed through as well), so we can write stuff like module MissionControl #... [Proc, Method, Binding, UnboundMethod].each_with_object(self, &:include) end if one were so inclined.
- drewbug01 3y agoHuh - truly did not know about _negative_ arity in Ruby, and what that actually means. I learned something today, thank you!
- dragonwriter 3y ago> In Haskell, by way of comparison, any one-parameter function can be used as the argument to map Ruby doesn’t have functions, but any one parameter block (to which any one parameter callable, and some other things, can be converted with an &) can be passed as the block parameter to map. And, yes, you can use function composition and similar operations to build up more complex callables from existing ones without an explicit new block or lambda. Your examples mostly have pretty close parallels in ruby: irb(main):044:0> ["hello", "goodbye"].map &:reverse => ["olleh", "eybdoog"] is the more compact form of the form with the explicit lambda: irb(main):045:0> ["hello", "goodbye"].map {|s| s.reverse} => ["olleh", "eybdoog"] composition works similarly, though there is no compact notation to convert an operator and its right operand to a callable, only the left: irb(main):048:0> ["a","bb","ccc","dddd"].filter &(2.method(:<) << :length.to_proc) => ["ccc", "dddd"] But, you do have to cheat a little to do: irb(main):049:0> [[1,2],[3,4]].map &:*.to_proc.uncurry => [2, 12] in place of: irb(main):049:0> [[1,2],[3,4]].map {|x, y| x * y} => [2, 12] Because while ruby has Proc#curry, it lacks Proc#uncurry. But its simple to define: class Proc def uncurry ->(args) { self.call *args } end end
- jonahx 3y ago> ghci> filter (\x -> (length x) > 2) ["a","bb","ccc","dddd"] ["ccc","dddd"]... I find the Haskell syntax concise and clear... > ["a","bb","ccc","dddd"].filter &(2.method(:<) << :length.to_proc) I can't imagine a stronger case for GP's thesis. The point-free ruby rewrite is so baroque and needlessly circuitous... the extra variable is a far lesser evil, and more concise: ["a","bb","ccc","dddd"].filter { |x| x.length > 2 }
- inopinatus 3y agomy instinct is ["a","bb","ccc","dddd"].grep /.../ but then, I'm a recovering perl hacker
- jonahx 3y agoLove it!
- agumonkey 3y agoI don't know how people feel about point-free these days. It was cool at first, then hated quite a lot due to issues I forgot (maybe a case or write-only code practice), but I find there's a high technical value and poetry to craft terms that plug together well with near no linking/binding declaration.