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 numhttps://clojuredocs.org/clojure.core/-%3E
It does the same thing, but it's much more awkward to use.
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.
> 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.
They enable you to write pipe transformations and in my opinion make the code much more readable.
https://stackoverflow.com/questions/2177110/in-f-what-does-p...
See e.g. https://shuangrimu.com/posts/language-agnostic-intro-to-depe...
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.
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.
That's essentially what 'IO' types are for (or finer-grained alternatives like 'Eff', etc.). At the value level, we have things like 'do notation', 'for/yield', etc.
h(x).f().g()
Is also very common and I would say it counts, 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 although I've not personally used a language which supports it :(
https://tour.dlang.org/tour/en/gems/uniform-function-call-sy...
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.
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
(-> x f g h ...)
[0] https://www.cs.cornell.edu/courses/cs3110/2019sp/textbook/ho...
[1] https://tour.dlang.org/tour/en/gems/uniform-function-call-sy...
[0] https://flix.dev/principles/
[1] https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax?w...
Every C-like language should copy this idea, imo.
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.
edit: The extension method page even links https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax
https://hackage.haskell.org/package/flow-1.0.22/docs/Flow.ht...
- There's workarounds. Eg. in F# you can redefine the pipe operator in debug mode [0] so it can be stepped into and inspected.
- I think some IDE plugins are also working on allowing pipeline contents to be automatically inspected.
- And of course, there's the good old tee operator for printf debugging. FP style rarely mutates variables, so a few print statements are just as good as a live object inspector.
As for why you actively wouldn't want to have those variables... much like comments, sometimes variable names add precious information and should be typed out, but sometimes they're just unnecessary noise, mental overhead, and scope pollution.
E.g. compare:
let suppliers = getSuppliers()
let sortedSuppliers = sortBy getSupplierPriority suppliers
let bestSupplier = first sortedSupplier
to: let bestSupplier =
getSuppliers()
|> sortBy getSupplierPriority
|> first
[0] https://stackoverflow.com/a/4966892I 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.
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.
It's not at all surprising that perl put thought into things like this, the entire language concept optimises for elegance of expression.
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.
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.)
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
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.
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)* In my office, but I am at home. Don’t remember the author; sorry. But it was written back in the ‘60s, I think.
article = create(author("John"), Page.article())
For cases where you're modifying data sequentially, the pipe structure makes it really easy to read: 5th_prime = 0.. | filter(is_prime) | nth(5)
Having a way to write function calls forwards and backwards lets you choose whatever syntax feels most appropriate for the job. myhashmap.entrySet()
.stream()
.map(Map.Entry::getValue)
.sorted()
.distinct()
.toList()1. can't be extended.
myhashmap.entrySet()
.stream()
.flattenBars()
.toFoo()
2. (As a result of 1) - applies only to streamsThey 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.
‘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.