9 ms·
Some of the weirdness is so completely avoidable that it frustrates me, but languages and libraries dig in on ‘you get used to it.’ I’m talking about the f(g(h(
by 6gvONxR4sf7o 5y ago
Some of the weirdness is so completely avoidable that it frustrates me, but languages and libraries dig in on ‘you get used to it.’ I’m talking about the f(g(h(x))) vs x | h | g | f. Right to left chains of functions seem more popular than chains of pipes in FP, but even being someone who has drunk the kool-aid deeply, I always prefer pipes.
‘Take the pan out of the over once it’s been in for 30min, where the pan contains a folded together eggs and previously mixed flour and sugar’ isn’t more FP than ‘Mix the flour with the sugar, then combine with eggs, then fold together, then put it into the oven, then take out after 30min.’ But people new to it so often think it’s an FP thing for no good reason. It’s a new user issue that ought to be totally fixable as a community.
FP has enough great ideas that I’d recommend everyone learn pure functional solutions just to put into their tool belt, but it’s absolutely true that getting up to speed is harder than it needs to be. My hot take: it won’t be really mainstream until someone figures out how to make dependent types really ergonomic, which seems a long way away.
- davidatbu 5y agoI'm relatively an FP newbie, and I have a few questions. 1. I've never seen the "pipe" notation you talk about in any lang except bash, which is not an FP right? In which languages does "|" denote function application? 2. Aren't dependent types orthogonal to whether a language is functional? Just to make sure we're on the same page on what dependent types mean, I wanna give a example (a contrived one, sorry) of dependent types in Python. from typing import overload, Literal @overload def str_if_zero(num: Literal[0]) -> str: ... @overload def str_if_zero(num: int) -> int: ... @overload def str_if_zero(num: int) -> int | str: if num == 0: return "That's a zero" return num
- lostcolony 5y agoElixir, Elm, Ocaml, Haskell, and F# all have a pipe like operator for function composition (though the actual operator varies).
- madcaptenor 5y agoR also has it, if you count R as a functional language.
- 6gvONxR4sf7o 5y agoI think libraries are just as relevant as languages, and dplyr’s pipes are wildly popular and (more to the point) even idiomatic R at this point.
- madcaptenor 5y agoActually base R has a pipe operator now. (I still use the dplyr pipe, just out of familiarity.)
- uoaei 5y agoAs does Julia
- pharmakom 5y agoR is a surprisingly functional language!
- davidatbu 5y agoGot it. Thanks!
- thaumasiotes 5y agoClojure has the -> macro: https://clojuredocs.org/clojure.core/-%3E https://clojuredocs.org/clojure.core/-%3E It does the same thing, but it's much more awkward to use.
- eigenhombre 5y agoAs a daily user of -> and ->>, I am curious what you find awkward about them.
- 5y ago
- mdoms 5y agoF# and Ocaml are good examples from my limited functional experience. https://stackoverflow.com/questions/2177110/in-f-what-does-pipelining-mean/2177129 https://stackoverflow.com/questions/2177110/in-f-what-does-p...
- davidatbu 5y agoThanks!
- 1-more 5y agoThe pipeline operator |> exists in Elm[0]. It is available in bash as & and the popular (in my limited experience) package flow[1] also implements it as |> for readability. It is a proposed addendum to Javascript[2] but I have no idea how seriously the JS powers-that-be are taking that. The example you gave of Python doesn't have tagged union. I really don't know much at all about Python so maybe the type checker can tell you that you're handling all the cases, but with tagged union types you can have to handle every possible output of `str_if_zero` when calling it. That doesn't have to be an FP thing but I only find it when doing Elm and Haskell in my experience. [0] https://elm-lang.org/docs/syntax#operators https://elm-lang.org/docs/syntax#operators [1] https://hackage.haskell.org/package/flow https://hackage.haskell.org/package/flow [2] https://github.com/tc39/proposal-pipeline-operator https://github.com/tc39/proposal-pipeline-operator
- 1-more 5y agoFor some reason I wrote "bash" when I meant "Haskell" in the second sentence. Sorry about that.
- agumonkey 5y agooutside `|` there's the threading macro in lisps (or else) (-> x f g h ...)
- deleted 5y ago[deleted]
- kjeetgill 5y ago> which is not an FP right? > Aren't dependent types orthogonal to whether a language is functional? You're correct and I think that's kinda the parent's point. I think what the OP is saying is that the much of the syntax ergonomics are orthogonal to FP, which means they don't have to be as weird/frustrating/unusual/insert preference/unergonomic as they are. A good example is how piping vs nesting are different syntaxes for function composition: x|A|B|C vs C(B(A(x))) The former might feel more comfortable, familiar, and left-to-right readable for someone new to FP. That said I think a few language do use piping. F# has |> and I think Clojure used >>> maybe? -- signed, humble java programmer.
- chitowneats 5y agoClojure has two primary macros for this, thread-first (->), and thread-last (->>). They enable you to write pipe transformations and in my opinion make the code much more readable.
- thaumasiotes 5y ago> piping vs nesting are different syntaxes for function composition: x|A|B|C vs C(B(A(x))) > The former might feel more comfortable, familiar, and left-to-right readable for someone new to FP. C(B(A(x))) is the syntax introduced in school, but note that if you continue on in algebra the notation flips from C(B(A(x))) to x_{ABC}. Everyone agrees that it's easier to list the transformations in the order they happen.
- chadrs 5y ago(2) No, dependent types are not the same as overloading. Basically it's when the type depends on the values; so like having a type for odd numbers or having an array that knows its length at compile time (for example to avoid array out of bounds exceptions at compile time)
- 6gvONxR4sf7o 5y ago1. Others have answered it well. Tons of languages have pipe operators. I used | in my example because I figured more readers will recognize bash piping than other notations. 2. Yes that's what I mean by dependent types, where types depend on values, like f(0): string and f(1): int. And yes, they totally are orthogonal. However, pure functional programming tends to lean on its type system more than other styles, in my experience. But then without dependent types, something as simple as loading a CSV gets hairy relative to something like pandas's pd.read_csv. Maybe others can go into why pure FP and static typing go so much together, but I gotta get back to work.
- deleted 5y ago[deleted]
- dwohnitmok 5y agoDependent types are not totally orthogonal of FP. They generally require immutability and function purity to work well and become extremely difficult to work with in the absence of those features. See e.g. https://shuangrimu.com/posts/language-agnostic-intro-to-dependent-types.html#why-functional https://shuangrimu.com/posts/language-agnostic-intro-to-depe...
- guerrilla 5y agoYeah, type checking becomes undecidable otherwise and the compiler can also become vulnerable but Rust says they're going to try, so we'll see.
- dwohnitmok 5y agoWell it's luckily a little better than that. Type checking doesn't have to be undecidable and the compiler doesn't have to be vulnerable because you don't have to execute any side effects in the compiler even if the code is meant to execute any side effects at runtime. It's more that the utility of dependent types is generally restricted to functions that are pure and deal with immutable values, which means one possibility is to specially mark which of your functions are allowed to be used in a dependent type and which aren't. But the fragment of your program that can be used in dependent types is generally going to be a pure FP fragment.
- guerrilla 5y agoSide effects aren't the only reason the compiler can be undecidable though. It depends on the type system, not just referential transparency. Look at Cayenne or ATS. Also, don't forget it's possible to still possible to DoS the system (although this is already more than possible with C++ and such, so what are we complaining about.) Yeah, dependent types would be nonsensical for code that isn't functional. Marking which parts are and aren't is a good idea. Idris did something similar for totality checking. I think it'd be nice to have a system which differentiated between functions and procedures though.
- gpderetta 5y agoh(x).f().g() Is also very common and I would say it counts, especially if the language support something like extension methods.
- chriswarbo 5y ago> especially if the language support something like extension methods Extension methods are a bit of a hack (although useful!); a cleaner approach is https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax although I've not personally used a language which supports it :(
- gpderetta 5y agoI'm a big fan of UFC myself.
- chromatin 5y agoI don’t know if I would say it’s very common(?) but it was introduced as a named concept (“uniform function call syntax”) AFAIK in D and is also a feature of Nim https://tour.dlang.org/tour/en/gems/uniform-function-call-syntax-ufcs https://tour.dlang.org/tour/en/gems/uniform-function-call-sy...
- chromatin 5y agoSome languages like OCaml have “pipeline” operator like `|>`, others like D allow you to chain function application `f.g.h()` instead of `h(g(f()))` [0] https://www.cs.cornell.edu/courses/cs3110/2019sp/textbook/hop/pipelining.html https://www.cs.cornell.edu/courses/cs3110/2019sp/textbook/ho... [1] https://tour.dlang.org/tour/en/gems/uniform-function-call-syntax-ufcs https://tour.dlang.org/tour/en/gems/uniform-function-call-sy...
- kjeetgill 5y ago> FP has enough great ideas that I’d recommend everyone learn pure functional solutions just to put into their tool belt Completely agree! I got my first taste of FP writing unreadable nests of python list comprehensions and it's shaped my Java tremendously. I've never spent much time in FP dedicated languages, but it's as good a frame to understand as OOP. I find they even mix and match well.
- the_gipsy 5y agoFor the order of fn composition, I found the pipe operator found in e.g. Elm really nice.
- LinAGKar 5y agoThat sort of chaining seems a bit like the method chains you have in some languages, like Java streams, C# LINQ and Rust iterators. That sort of "functional constructs when it fits" is getting popular, even if pure functional isn't gaining much popularity.
- scns 5y agoIn D, Flix [0] and Nim you can use UFCS [1] to chain functions. [0] https://flix.dev/principles/ https://flix.dev/principles/ [1] https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax?wprov=sfla1 https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax?w...
- turkeygizzard 5y agoI think maybe part of it is that most people are introduced to FP through haskell, if I had to guess. And haskell (at least the tutorials) is IMO guilty of the top-down approach where the final value is written first, referencing variables that haven't appeared yet. ocaml, on the other hand, has you define your components before the final value which is more familiar to most programmers I think haskell doesn't always look super readable, but I can appreciate it at least because it reminds me of math papers where the final theorem is introduced first, and then it's broken into lemmas, and each of those lemmas is proved etc
- scns 5y agoI concur, in Haskell you have to import the pipe operator (&) before using it, kinda second class, no snark intended.
- fiddlerwoaroof 5y agoI’ve “drunk the koolaid” and I mostly went the opposite direction on left-to-right pipes vs. right-to-left compose: with a(b(c(d))), order of evaluation will always be arguments->function call (in most languages) and, so d is the first thing evaluated. Pipes make it seem more consistent, but it also introduces an inconsistency.
- grapist420 5y agoisn’t the first thing you are being the first thing evaluated just better? we read ltr, so having to see and remember a before c(d) but then having some of a’s arguments at the very end is just annoying vs pipes
- fiddlerwoaroof 5y agoIt's hard for me to verbalize, but the rtl composition just sort of makes sense. For one thing, it's the way functions already sort of work: a(b(c(d))) === (a • b • c)(d) // the functions are in the same order And, when you think about the typical order of evaluation, a(b(c(d, e, f))) is evaluated: - look up the value of d, e and f - evaluate c(d, e, f) (call it g) - evaluate b(g) (call it h) - evaluate a(h) (call it i) - return i Pipes add an inconsistency between the order of evaluation of arguments vs. function calls and the order of evaluation of functions. (Also, completely separately, I've mostly come around to disliking operators and thinking Lisps are right here: precedence and associativity are "intuitive" for basic math operators because we've spent years studying math and getting used to PEMDAS, but anything beyond this is just asking for trouble: aside, maybe, from APL/J's strict right-associativity without precedence)
- hanche 5y agoI have an algebra book on my shelf* that puts function arguments on the left, as in (x)f instead of the usual f(x). It is definitely more “natural”, as functions compose in the normal reading direction. But even so, it is increadibly hard to read, because the break with the usual convention is so radical. Needless to say, it didn’t catch on. * In my office, but I am at home. Don’t remember the author; sorry. But it was written back in the ‘60s, I think.
- deleted 5y ago[deleted]
- grapist420 5y agoI’ve hacked pipes in python many times just because it’s so much better. And no, “a=f(a);\n b=g(a);\n c=h(b);\n a=i(c)” isn’t “more declarative” or “more expressive” than a | f | g | h, and even in python a |pipe| b |pipe(blah)| c |pipe(blah, blah)| d is so much nicer than nesting the parens.
- lpapez 5y agoThis sounds like a debugging nightmare. Is it not easier to name each intermediate result so you can inspect it in a debugger?
- Kache 5y agoIn terms of debugging inconvenience for intermediate values, `->(x) | a | b | c` is the same as `c(b(a(x)))`. I find debugging /w a REPL using functional-esque code much easier. You can play around and exec/inspect lines/snippets at will without changing state & "breaking" the debug session.
- lpapez 5y agoAgreed they are pretty much the same inconvenience level, but for me that level is too much either way. Maybe it's just my workflow, but I always preffered step-by-step debugging compared to REPL which is why I like to see many intermediate results being named.
- grapist420 5y agowhen I hack it into python, it’s slightly annoying. when done properly, no, it’s no worse than f(a+g(b+c())), which is everywhere in code already. And debuggers / error messages should indicate what sub expression caused it, which would clean that up
- acdibble 5y agoElixir includes IO.inspect[1], which prints a value and then returns it. It makes it easy to insert this function into a pipeline without disrupting the existing logic[2]. [1] https://hexdocs.pm/elixir/1.12/IO.html#inspect/2 https://hexdocs.pm/elixir/1.12/IO.html#inspect/2 [2] https://elixir-lang.org/getting-started/debugging.html https://elixir-lang.org/getting-started/debugging.html
- FeepingCreature 5y agoD doesn't have pipes, being a C-like (it probably could, but it's untypical), but it has something almost as good: "uniform function call syntax", meaning that a function that takes X as its first parameter can be called as if it was a method of X. `foo(x)` => `x.foo()`. This makes functional programming in D a lot closer to the pipe example, in that you're usually passing a lazy iterator ("range") through a sequence of chained calls. Every C-like language should copy this idea, imo.
- sk0g 5y agoThat sounds amazing, but I wonder if it could get unwieldy as well. Would have absolutely loved it in Go, where over the course of time I had to build up dozens if not hundreds of validators on strings, maps, etc. Ended up resorting to storing them in `companyInitials-dataType` packages, EG `abcStrings.VerifyNameIsCJK()`, but the unified call syntax would be super tidy.
- asdasdaergwerh 5y agoThis sounds like https://en.wikipedia.org/wiki/Method_chaining https://en.wikipedia.org/wiki/Method_chaining AKA https://en.wikipedia.org/wiki/Fluent_interface https://en.wikipedia.org/wiki/Fluent_interface
- FeepingCreature 5y agoYep exactly, but without requiring monkeypatching or operator hacks. Basically sorta like what C# does. edit: The extension method page even links https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax
- _dain_ 5y agoNim has this as well, it's lovely.
- theCodeStig 5y agoA lot of the difficulty in learning FP is in unlearning imperative programming. I hypothesise that for someone whom has never programmed before, FP and declarative paradigms are easier to reason about.
- qsort 5y agoJava streams are a (limited) version of what you propose. Something like this is very idiomatic: myhashmap.entrySet() .stream() .map(Map.Entry::getValue) .sorted() .distinct() .toList()
- carlmr 5y agoI was going to say C# LINQ, too!
- kilotaras 5y agoDifference with pipe operator would be 1. can't be extended. myhashmap.entrySet() .stream() .flattenBars() .toFoo() 2. (As a result of 1) - applies only to streams
- qsort 5y ago> can't be extended They can be extended arbitrarily by defining collectors. The Stream interface is very well designed. For example your "toFoo" would be .collect(Collector(<foo-provider>, <foo-accept>, <foo-merge>, <foo-finisher>)). All of them can be arbitrary functions working on arbitrary types. That's the standard library implementation of "toList", in fact! > applies only to streams You're exactly correct here, but for legacy reasons Java did not have a choice. I'm the first to admit mylist.stream().map(f).toList() is dumb. A list is a functor, dammit! It's the best possible solution given the existing constraints, but it's definitely limited in what it can do.
- Siira 5y agoMost FP-heavy languages also have good support for metaprogramming, and have macros that imitate pipes (e.g., all lisps and Julia).
- sullyj3 5y agoThis is why every time this stuff comes up I plug the haskell flow package. Imo, it should be in the prelude. https://hackage.haskell.org/package/flow-1.0.22/docs/Flow.html https://hackage.haskell.org/package/flow-1.0.22/docs/Flow.ht...
- thaumasiotes 5y ago> ‘Take the pan out of the over once it’s been in for 30min, where the pan contains a folded together eggs and previously mixed flour and sugar’ isn’t more FP than ‘Mix the flour with the sugar, then combine with eggs, then fold together, then put it into the oven, then take out after 30min.’ Oddly enough, the language that put the most thought into this problem is Perl, which provided the pronoun variables to represent "whatever I just computed". Natural languages always offer multiple strategies for ordering the various parts of a sentence, because the order in which things are mentioned is critical for several purposes, from making it easier for the listener to follow a chain of thoughts to building or defusing tension or correctly positioning the punchline of a joke.
- pnut 5y agoI'm always surprised how unpopular perl (and even more, its spiritual successor raku) is with the HN crowd. It does not square with my experience of various languages' ergonomics. It's not at all surprising that perl put thought into things like this, the entire language concept optimises for elegance of expression.
- toxik 5y agoI can't speak for Perl's general usefulness, but pronoun variables are just not terribly useful, because you can make them up on the spot. For example, result = input result = transform(result) result = transform2(result) return result Perhaps I misunderstood the idea of pronoun variables. A related idea is the idea of `self` or `this`, in that computation is viewed from the perspective of an agent (which is ironically called an object when it is typically a grammatical subject). Perhaps this is why we like OOP and CSP patterns so much, it meshes well with our social abilities.
- raiph 5y ago> Perhaps I misunderstood the idea of pronoun variables. Their point is the same as the one in human language. You can't make them up on the spot -- they already exist, with predetermined reference logic that we learn as part of learning a language. All you can do is mention them. This means they often reduce both the reading and comprehension load in comparison with inventing a new name and binding it to some referent. For example, in this sentence: > I can't speak for Perl's general usefulness, but pronoun variables are just not terribly useful, because you can make them up on the spot. Who's "I"? And "you"? What's "them"? These are all obvious. Compare that with: > I = the person writing this. This = the writing you are reading. You = the person reading this. I can't speak for Perl's general usefulness, but pronoun variables are just not terribly useful, because you... [You = someone writing code] can make them [Them = pronoun variables (though, as noted, you can't make them)] up on the spot. Consider what it would take to write code displaying the first 10 numbers in the fibonacci sequence in some PL. Here it is in Raku using pronouns: say .[^10] given 0, 1, *+* ... Inf # (0 1 1 2 3 5 8 13 21 34) I grant that you have to learn this aspect of Raku to be able to read and write the above code. But it only takes a few seconds to learn that: * `.foo` means applying `foo` to "it". * `[^10]` means up to the 10th element. That's got nothing to do with pronouns, but whatever. * The `*` in `*+*` is the pronoun "whatever" and, when used in combination with an unary or binary operator forms a lambda for that operation. So `*+*` reads as "whatever plus whatever" and represents a two argument lambda that adds its arguments. * `...` is Raku's "sequence" operator, which uses the preceding function/lambda as the generator (which in turn uses the preceding N arguments per the generator's arity). Again, nothing to do with pronouns, and yet more you have to learn, but that's how PLs are. It will no doubt look awfully weird, almost as weird as, say, Chinese (presuming you don't know Chinese). But that (it looking weird) is something completely distinct from whether it (using it) is extremely pleasant once you just accept it. With a sufficiently open mind it (accepting using a "whatever" pronoun) can take literally a few seconds. (Thus kids tend to find this notion of "whatever" in Raku easy to grok, whereas adults sometimes struggle.)