Some thoughts as I read through this (I'm far from an expert in PL, so... forgive me any naivety, please).
I can't decide if I like the pipeline operator (1). Is it really better? Perhaps I'm just used to chaining multiple function calls together.
Pattern-matching (2) is my favorite language feature, or at least it's pretty high up there. Super useful. I always wish I had it in Python (my go-to language most of the time).
Not sure I really "get" reactive programming (3).
With regard to implicit names (4), I think I actually prefer writing lambdas with my own variable names, and I particularly like Haskell's syntax for it.
Even Scala's usage of the underscore seems a little better than this "it" business, if only because the underscore doesn't see much use elsewhere so it's an indicator that "Hey, something's going on here!"
But using a regular-looking variable name seems a bit unintuitive for somebody unfamiliar. Though I suppose they may not be the people for whom this sort of code is written.
Isn't destructuring (5) just an effect of ADTs? Which are also what produce (2) pattern-matching, so perhaps really what the author enjoys is ADTs. Or maybe I don't know things.
No strong feelings on (6). Maybe I'd need more exposure.
Are if-expressions (7) actually on this list? In the comments they practically suggest that the ternary conditional is the "regular" way of handling such things, which is... silly. The rest of this list seemed interesting, so I'm confused why these make the cut. Perhaps the author was just being cheeky.
Try-expression (8) are nice and all, but the best is when you can eschew them completely. I like the way optionals are wrapped into so much in Swift; almost makes obsolete all the try-expressions (except for certain cases, such as whenever you call into an older Obj-C library or something).
Definitely a fan of auto-currying (9).
Is method extension (10) similar to overriding the "magic" double-underscore methods in Python, such as `__add__`? That definitely seems useful.
On the other hand, we are used to understanding such "backward" pipelines, so your mileage may vary.
> With regard to implicit names (4), I think I actually prefer writing lambdas with my own variable names
I would agree, especially because "it" is not a very good name in some cases. "it.length == 5" is really jarring. In Smalltalk, the custom is to name the variable "each", so for the article's examples you would get "each length = 5" and "each toUpperCase", which look nicer IMHO.
> Isn't destructuring (5) just an effect of ADTs?
Do you mean algebraic data types? I think ADT typically refers to abstract data types, but I'm old. Anyway, yes, destructuring is just syntactic sugar for a pattern match where there is only one variant, so the match can never fail.
> Definitely a fan of auto-currying (9).
I'm a fan of writing and using curried functions by default. I'm not a fan of Reason's syntax that suggests the opposite.
> Is method extension (10) similar to overriding the "magic" double-underscore methods in Python, such as `__add__`?
No. It is adding a new method to an existing class from someone else's library.
I'm not sure about mixing prefix (for functions), infix (for operators) and postfix (for pipelines) notation in the same languaqe. Does having all three in the same language actually help readability? If you really want to read things forward, you could always adopt forth, where everything is postfix:
Yes, but that would be throwing the baby out with the bathwater. Just because of a single operator that I'm not sure is useful in a language I'm otherwise satisfied with.
> where everything is postfix
Except for parsing words, which are commonly used to change exactly this property of the syntax, no?
The fact that postfix is nice in many cases does not imply that postfix is always a good idea.
Hmm that's a good point about the pipeline operator. But I'm not sure how I feel about having both idioms readily available. In which cases would you choose forwards processing instead of backwards, you know? I'll have to think more about that.
> In Smalltalk, the custom is to name the variable "each"
Ooh I like that! I think I would like it even more if "each" was a reserved word, like "this" in e.g. Java. But maybe that's a bit much.
> Do you mean algebraic data types?
Yes, sorry. I talk to functional people a lot so we tend to use "ADT" for the algebraic ones (those FP people are all about brevity it seems).
Glad to have that confirmed, though. Just wanted to be sure I wasn't missing something.
> I'm a fan of writing and using curried functions by default. I'm not a fan of Reason's syntax that suggests the opposite.
Could you clarify a bit? I'm not sure I follow exactly what you mean about the syntax being unclear. It seems similar to Haskell, except with parentheses, which doesn't seem terrible.
> No. It is adding a new method to an existing class from someone else's library.
Oh I see! I totally misread that example. Thanks for the clarification!
> It seems similar to Haskell, except with parentheses
That's the point: In Haskell, the parentheses (and especially the comma) are meaningful, and "foo(x, y)" is not the same as "foo x y". Reason jumps through extra hoops to make the two syntaxes mean the same thing. To a Haskell (or OCaml, or other ML family) programmer, the Reason syntax suggests an uncurried function.
You can do arity overloading with curried functions if you allow overloading the function application operator itself. E.g. if f is of type A → B and a is of type Decoder A then map f a is of type Decoder B and in the special case where B is a function type C → D, that Decoder B = Decoder (C → D) can be applied to a value c of type Decoder C making map f a c a Decoder D.
Caveat: I'm not aware of any language that actually does this, likely because redefining the core syntax of function application would be so easy to abuse.
EDIT: Removed my Haskell example, because I think I got it wrong.
In my code, the main use of arity overloading is to implement default parameters in Java, i.e., the feature has no intrinsic value, it's a workaround for the missing feature of default values; if a language has reasonable support for default parameters then arity overloading can go, leaving only type-dependent overloading.
At least in F#, I think that having too much need for arity overloading might be considered a code smell. In a nutshell, the idea is that, if you've got a bunch of functions with the same name but different arities, collectively they add up to a violation of the single responsibility principle.
If you do need it, though, you can make a 1-arity function whose only argument is a tuple of a given size. This is how methods from C# and VB libraries are presented. I believe that it just compiles to a non-curried function behind the scenes.
As well as the readability advantages, a major reason for the pipeline operator in F# (the first place I encountered it) is to help with type inference. Because type inference happens left to right in F# the compiler usually knows the type you're dealing with already with code written using the pipeline operator but you may have to manually specify the type without it. It also makes for a better intellisense experience.
>With regard to implicit names (4), I think I actually prefer writing lambdas with my own variable names, and I particularly like Haskell's syntax for it.
Introducing implicit variable names is my most hated "modern" feature. I still explicitly write down the default name.
The only defensible way to implement this is by making "it" a keyword and allowing you to access the outer scopes with a syntax like "outer.it" the same way some languages support "outer.this" or "outer.super" when you want to explicitly access the parent class of a nested class.
Regarding pattern-matching, if Python is your go-to programming language and you are looking for convenient pattern matching, you should check out Coconut(http://coconut-lang.org): it's a superset of Python 3 that transpiles to any target version of Python, which adds a lot of useful features like pattern-matching.
He can't find the documentation for the pipeline operator.
And he posted examples from ReasonML rather than F# and OCaml.
For reference this is the documentation for the pipeline operator in F#:
https://msdn.microsoft.com/visualfsharpdocs/conceptual/opera...
It's kind of sad that people nowadays know only JavaScript and languages from the web ignoring the original language from where the syntax come from.
I would not be surprised if he posted some purescript instead of Haskell.
As per the "modern" languages in the title the author has no idea about what he is speaking about.
OCaml is 21 years old, just 1 year younger than Java, I would not call it a "modern" language.
But if someone thinks that the code he posted exists only in ReasonML than that one can be defined a modern language.
I rarely criticise blog post, but this one is just not well written and the author has no idea about this subject.
> but this one is just not well written and the author has no idea about this subject.
Rubbish. The examples are good. The author comes across as enthusiastic. The writing style is fine and engaging. A couple of the examples are from a modern variant of an older language but so what? That small oversight and you completely bash the effort?
You could have said,
"The writing didn't do it for me and the author has more to learn about the subject" but you came out with the big stick instead. You come across like a typical elitist functional programmer. Blah, blah, blah, Lisp had that _forty_ years ago, lame!
I thought the language features were interesting. I'd read up on Kotlin but overlooked the it keyword, I'd love if Ruby got that. In fact, I was trying to figure out which features Ruby has the whole time. Ruby blocks (closures?) are cool with the || operator but having a default it keyword, we should totally steal that. In total I think Ruby has #5 Destructuring, #7 If Expressions, #10 Method Extensions (called Open Classes in Ruby I think) – correct me if I'm wrong. Ruby kind of does pattern matching with the case statement/expression. The Pipeline Operator looks cool. I don't think Ruby has anything like the Cascade operator either.
I'm sorry, I don't know what are you used to read, but writing an example and after that just pointing at the documentation (or at something completely unrelated like in the first example) is not an example of good writing.
You are forced to follow the link and it breaks your flow.
The it keyword is also not original in kotlin. Groovy has it for example.
The author is just getting random features that have been around for decades mistakenly believing that some modern language introduced it.
Your attack is completely non-sense, I don't know lisp and I don't like it.
Furthermore in my day to day job I write only OO code (sadly).
And I certainly don't think that my post is rubbish given that I explained the reasons for my criticism.
GP is right, and you're the one being harsh and offensive. It is indeed a shame to see people thinking 30 year old ideas are new because they are uninformed. Humanity is supposed to progress, not spin its wheels.
The post is indeed uninformed and poorly researched. I would rather not see it posted.
I updated the article to make more clear that examples are from modern languages, but many of the ideas come from older languages.
> All the examples above are from Reason, Swift, Kotlin and Dart. However, many of the ideas above can already be found in much older languages such as Lisp (1958), Smalltalk (1972), Objective-C (1984), Haskell (1990), OCaml (1996) and many more. So while the examples are from modern languages, the ideas in this article are actually very old. (*)
> You could say that those “modern” languages are trying to popularise old ideas. They put the idea in a different more common syntax while also leveraging older ecosystems. All the languages in this article use the popular C-style syntax. For example, Reason is OCaml in a C-style/Javascript-style syntax while also leveraging the JavaScript and OCaml ecosystem and Dart is heavily influenced by Smalltalk and leverages the Javascript ecosystem.
Also interesting: goroutines (Go), borrowing semantics (Rust), various forms of concurrency (e.g. blocks in ObjC/Swift with GCD, mutex closures, futures, etc.).
It would be possible to do, but it hasn’t been proposed as an ECMAScript feature nor do I know of any plugins that implement it, so you’d have to write the plugin yourself.
They basically serve the same function, however since many functional languages don't have method chaining they use the pipe operator instead. I honestly don't know why anyone would have a strong preference for one form over the other
- It's more extensible: You can defines functions over standard data types anywhere, you can't define methods for standard data types anywhere,
- It's more flexible on input types: You can deal with sum types without polluting your classes with flags.
- It's more flexible on execution order: You can shortcut things, you can define your operator in a way that it runs some things twice, you can jump over some function (if the typing allows).
- It's more composable: You can intercalate this with other operators.
- It's data: You can input the sequence into a function, rewrite it and execute the new sequence, interpret it and create some documentation, run some offline verification without executing the code, etc.
It's not a replacement for method chaining, and can't even be used for that purpose. It's for rearranging the relative position of a function's arguments. In pseudocode, it's defined like:
x |> f ->. f(x)
So, applying that, it would let you transform something like
exclaim(capitalize(doubleSay("hello")))
to your first example. But it can't do anything with the 2nd example, because those are not functions with arguments, they're all 0-arity method calls.
More generally, I don't think this operator makes much sense in a language that's mostly object-oriented. I made one in Scala a while back and toyed with it, but even there it's pretty useless because the libraries follow a very object-oriented coding style. I've really only seen it shine in a language that has strong ML roots and tends to stick to them.
> those are not functions with arguments, they're all 0-arity method calls
In other words, they are 1-argument function calls. The "this"/"self" parameter is implicit in many languages, but otherwise it's not different from a normal function parameter. In the "method chaining" version of the example, each method call returns a (reference to) a temporary object which is passed as the next method call's "this" parameter.
In most languages, that's an implementation detail that is hidden by the compiler, and not a part of the semantics of the language itself.
More specifically, if the language won't allow me to do both:
foo.method(x)
and
method(foo, x)
and have them behave the same way, then I submit that, regardless of what's going on behind the curtain, a n-arity method is not really the same thing as a (n+1)-arity function in that language.
Are there any dynamic languages that have automatic currying? I agree that that would be dangerous, but it's also something that I've literally never seen.
The "implicit it" is called anaphor in Lisp (though using that term for a programming language feature I think only goes back to the 1990s; the idea itself is older).
(The word "anaphor" here comes from the study of grammar in natural languages.)
All these new language features are good taken on their own, but in the big picture many languages are getting relentlessly more complex.
Many are getting to the point the complexity encourages different programming styles within one language that while syntactically valid can be so different that other programmers can't easily understand them (such as C++), which is a problem for delivering business value.
Cohesiveness and simplicity is a feature, that delivers real and significant business value.
I tend to agree. At the two extreme ends of the spectrum I'd place Go and Scala. Go very simple, Scala very complex.
What happens in practice is that Go code looks all the same, it's easy to program in, reason about, and train new programmers in. It's not that expressive or flexible, but bad Go code really jumps at you.
Scala tries to be all things. The result is that it's not obvious what great Scala code is supposed to look like. Successful Scala teams tend to stick to a subset of the language, usually the more functional side.
I think languages like Clojure, Elixir, F#, and Kotlin offer a happier middle ground. Plenty powerful but not as cognitively taxing.
61 comments
[ 2.9 ms ] story [ 131 ms ] threadI can't decide if I like the pipeline operator (1). Is it really better? Perhaps I'm just used to chaining multiple function calls together.
Pattern-matching (2) is my favorite language feature, or at least it's pretty high up there. Super useful. I always wish I had it in Python (my go-to language most of the time).
Not sure I really "get" reactive programming (3).
With regard to implicit names (4), I think I actually prefer writing lambdas with my own variable names, and I particularly like Haskell's syntax for it.
Even Scala's usage of the underscore seems a little better than this "it" business, if only because the underscore doesn't see much use elsewhere so it's an indicator that "Hey, something's going on here!"
But using a regular-looking variable name seems a bit unintuitive for somebody unfamiliar. Though I suppose they may not be the people for whom this sort of code is written.
Isn't destructuring (5) just an effect of ADTs? Which are also what produce (2) pattern-matching, so perhaps really what the author enjoys is ADTs. Or maybe I don't know things.
No strong feelings on (6). Maybe I'd need more exposure.
Are if-expressions (7) actually on this list? In the comments they practically suggest that the ternary conditional is the "regular" way of handling such things, which is... silly. The rest of this list seemed interesting, so I'm confused why these make the cut. Perhaps the author was just being cheeky.
Try-expression (8) are nice and all, but the best is when you can eschew them completely. I like the way optionals are wrapped into so much in Swift; almost makes obsolete all the try-expressions (except for certain cases, such as whenever you call into an older Obj-C library or something).
Definitely a fan of auto-currying (9).
Is method extension (10) similar to overriding the "magic" double-underscore methods in Python, such as `__add__`? That definitely seems useful.
I think it can be useful for processing that is clearly separated into stages. You can read it forward:
instead of backward: On the other hand, we are used to understanding such "backward" pipelines, so your mileage may vary.> With regard to implicit names (4), I think I actually prefer writing lambdas with my own variable names
I would agree, especially because "it" is not a very good name in some cases. "it.length == 5" is really jarring. In Smalltalk, the custom is to name the variable "each", so for the article's examples you would get "each length = 5" and "each toUpperCase", which look nicer IMHO.
> Isn't destructuring (5) just an effect of ADTs?
Do you mean algebraic data types? I think ADT typically refers to abstract data types, but I'm old. Anyway, yes, destructuring is just syntactic sugar for a pattern match where there is only one variant, so the match can never fail.
> Definitely a fan of auto-currying (9).
I'm a fan of writing and using curried functions by default. I'm not a fan of Reason's syntax that suggests the opposite.
> Is method extension (10) similar to overriding the "magic" double-underscore methods in Python, such as `__add__`?
No. It is adding a new method to an existing class from someone else's library.
Yes, but that would be throwing the baby out with the bathwater. Just because of a single operator that I'm not sure is useful in a language I'm otherwise satisfied with.
> where everything is postfix
Except for parsing words, which are commonly used to change exactly this property of the syntax, no?
The fact that postfix is nice in many cases does not imply that postfix is always a good idea.
Isn't the pipeline operator infix?
> In Smalltalk, the custom is to name the variable "each"
Ooh I like that! I think I would like it even more if "each" was a reserved word, like "this" in e.g. Java. But maybe that's a bit much.
> Do you mean algebraic data types?
Yes, sorry. I talk to functional people a lot so we tend to use "ADT" for the algebraic ones (those FP people are all about brevity it seems).
Glad to have that confirmed, though. Just wanted to be sure I wasn't missing something.
> I'm a fan of writing and using curried functions by default. I'm not a fan of Reason's syntax that suggests the opposite.
Could you clarify a bit? I'm not sure I follow exactly what you mean about the syntax being unclear. It seems similar to Haskell, except with parentheses, which doesn't seem terrible.
> No. It is adding a new method to an existing class from someone else's library.
Oh I see! I totally misread that example. Thanks for the clarification!
That's the point: In Haskell, the parentheses (and especially the comma) are meaningful, and "foo(x, y)" is not the same as "foo x y". Reason jumps through extra hoops to make the two syntaxes mean the same thing. To a Haskell (or OCaml, or other ML family) programmer, the Reason syntax suggests an uncurried function.
One feature that always impressed me is how Kotlin has bring-your-own-chaining methods defined on everything.
Not the best example of how it's useful because I'm rusty, but it's nice to just chain your own transformations on anything. I miss it all the time.Caveat: I'm not aware of any language that actually does this, likely because redefining the core syntax of function application would be so easy to abuse.
EDIT: Removed my Haskell example, because I think I got it wrong.
If you do need it, though, you can make a 1-arity function whose only argument is a tuple of a given size. This is how methods from C# and VB libraries are presented. I believe that it just compiles to a non-curried function behind the scenes.
Introducing implicit variable names is my most hated "modern" feature. I still explicitly write down the default name.
The only defensible way to implement this is by making "it" a keyword and allowing you to access the outer scopes with a syntax like "outer.it" the same way some languages support "outer.this" or "outer.super" when you want to explicitly access the parent class of a nested class.
Rubbish. The examples are good. The author comes across as enthusiastic. The writing style is fine and engaging. A couple of the examples are from a modern variant of an older language but so what? That small oversight and you completely bash the effort?
You could have said,
"The writing didn't do it for me and the author has more to learn about the subject" but you came out with the big stick instead. You come across like a typical elitist functional programmer. Blah, blah, blah, Lisp had that _forty_ years ago, lame!
I thought the language features were interesting. I'd read up on Kotlin but overlooked the it keyword, I'd love if Ruby got that. In fact, I was trying to figure out which features Ruby has the whole time. Ruby blocks (closures?) are cool with the || operator but having a default it keyword, we should totally steal that. In total I think Ruby has #5 Destructuring, #7 If Expressions, #10 Method Extensions (called Open Classes in Ruby I think) – correct me if I'm wrong. Ruby kind of does pattern matching with the case statement/expression. The Pipeline Operator looks cool. I don't think Ruby has anything like the Cascade operator either.
This is a personal attack that's entirely uncalled for.
Also, the OP never said anything about Lisp. It is really not productive to bring such loaded assumptions into a comment thread.
Your attack is completely non-sense, I don't know lisp and I don't like it. Furthermore in my day to day job I write only OO code (sadly). And I certainly don't think that my post is rubbish given that I explained the reasons for my criticism.
It's super great that more and more people are learning about these cool features that have actually been around for a while.
It's super disappointing that none of this was new to me. Oh well.
The post is indeed uninformed and poorly researched. I would rather not see it posted.
> All the examples above are from Reason, Swift, Kotlin and Dart. However, many of the ideas above can already be found in much older languages such as Lisp (1958), Smalltalk (1972), Objective-C (1984), Haskell (1990), OCaml (1996) and many more. So while the examples are from modern languages, the ideas in this article are actually very old. (*)
> You could say that those “modern” languages are trying to popularise old ideas. They put the idea in a different more common syntax while also leveraging older ecosystems. All the languages in this article use the popular C-style syntax. For example, Reason is OCaml in a C-style/Javascript-style syntax while also leveraging the JavaScript and OCaml ecosystem and Dart is heavily influenced by Smalltalk and leverages the Javascript ecosystem.
1) pipeline operator: thread macro
2) pattern-matching: multimethods, core.match
3) reactive programming: Reagent/re-frame, atoms, core.async
4) implicit name 'it': %
5) destructuring: destructuring
6) cascade: .. macro
7) if: if
8) try: try
9) currying: currying
10) method extensions: protocols
- It's more flexible on input types: You can deal with sum types without polluting your classes with flags.
- It's more flexible on execution order: You can shortcut things, you can define your operator in a way that it runs some things twice, you can jump over some function (if the typing allows).
- It's more composable: You can intercalate this with other operators.
- It's data: You can input the sequence into a function, rewrite it and execute the new sequence, interpret it and create some documentation, run some offline verification without executing the code, etc.
More generally, I don't think this operator makes much sense in a language that's mostly object-oriented. I made one in Scala a while back and toyed with it, but even there it's pretty useless because the libraries follow a very object-oriented coding style. I've really only seen it shine in a language that has strong ML roots and tends to stick to them.
In other words, they are 1-argument function calls. The "this"/"self" parameter is implicit in many languages, but otherwise it's not different from a normal function parameter. In the "method chaining" version of the example, each method call returns a (reference to) a temporary object which is passed as the next method call's "this" parameter.
More specifically, if the language won't allow me to do both:
and and have them behave the same way, then I submit that, regardless of what's going on behind the curtain, a n-arity method is not really the same thing as a (n+1)-arity function in that language.foo.method(x)
Is equivalent[1] to
Bar.method(foo, x)
[1] in the absence of monkey patching on foo
"implicit 'it'" is my favorite: strings.filter{ it.length == 5 }
And Cascade operator buttons..hidden=true
Automatic currying is dangerous
Method extensions are not 'new' but nice
Why?
Well, I have just replied to somebody asking about the differences: https://news.ycombinator.com/item?id=15669886
> Automatic currying is dangerous
On languages that don't enforce numbers of arguments. That's Javascript, Python, and I don't think there's any other.
(The word "anaphor" here comes from the study of grammar in natural languages.)
Which appear in Arc.
In which this site is developed.
using myPoint .x = 1 .y = 1 end
a bit rusty on the syntax but that was the basics of it!
With myPoint
End WithMany are getting to the point the complexity encourages different programming styles within one language that while syntactically valid can be so different that other programmers can't easily understand them (such as C++), which is a problem for delivering business value.
Cohesiveness and simplicity is a feature, that delivers real and significant business value.
What happens in practice is that Go code looks all the same, it's easy to program in, reason about, and train new programmers in. It's not that expressive or flexible, but bad Go code really jumps at you.
Scala tries to be all things. The result is that it's not obvious what great Scala code is supposed to look like. Successful Scala teams tend to stick to a subset of the language, usually the more functional side.
I think languages like Clojure, Elixir, F#, and Kotlin offer a happier middle ground. Plenty powerful but not as cognitively taxing.