This example with the absence of an Eq class is not related to just equality, it’s a symptom of a lack of generics. You’ll end up with a lot of special, built-in classes (e.g. Ord), while just adding simple type classes to Elm would make all of this solvable in a library (e.g. Elm’s Prelude).
Surely, if we agree that Eq, Ord, and Foldable are useful, they’re probably not the only useful type classes in existence.
Ah, here comes the condescending tone I've so come to appreciate from the Haskell programmers.
"Go and read", "anti-intellectualism".
> You need a PhD in type theory to get anywhere.
That is extremely false. Haskell isn't even a good playground for academic type theory -- you'd want Agda etc. for that. The development of the language over the last few years has been characterized by pragmatism and a focus on backwards-compatibility, which is why you can take code from something like ten years ago and have it run without issues on modern versions of the Haskell compiler with little to no modifications. (Let's not talk about how long code written in "modern" JS lasts.)
And I'd really like to see type class constraint resolution with functional dependencies, or Hindley-Milner type checking, or something of that sort implemented in "5-10 lines" of JS.
"There he goes again with his mumbo-jumbo," you say. That's right, you don't need to care about those things to write Haskell. What you meant is implementations of typeclasses ("interfaces") like Monad, Functor, and so on: they don't take much more code in Haskell.
Array.prototype.chain = function (f) {
return this.reduce((acc, it) => acc.concat(f(it)), [])
}
instance Monad [] where
xs >>= f = concat (map f xs)
return = pure
And we didn't even have to go the "this" route! Notice that your 5 - 10 lines of JS don't let you write code that works in any monad, whereas I can easily write
whenM :: Monad m => m Bool -> m () -> m ()
whenM cond action = do
condition <- cond
if cond then action else pure ()
In Elm, you'd have List.whenM, Array.whenM, Maybe.whenM, ... or a straight-up false type signature like their Eq ones, and in JS, a bunch of prototype methods with no unifying threads.
--
As for an example of why I think Haskell has the right ideas (few of us will say it's the "best language evar"):
I'd really like to see a JS version of the Servant library, which takes an API spec for a server and actually generates a fully functional server from that. Here's a description:
https://news.ycombinator.com/item?id=14149200 https://news.ycombinator.com/item?id=14149200
Does this strike you as idle theoretical self-enjoyment?
> This is extremely false [referring to "You need a PhD in type theory to get anywhere."]
almost immediately followed by
> And I'd really like to see type class constraint resolution, or Hindley-Milner type checking
You don't even see the irony in that, do you?
> they don't take much more code in Haskell:
riiight. I won't even go into the number of things that need to be explained there before you even start explaining what the code does.
> Does this strike you as idle theoretical self-enjoyment?
It does.
How many PhDs does one require to understand/correct/debug all the :> and :<|> etc.?
Speaking from experience, zero.
> debug
But the whole point of a good compiler is that it tells you when you're wrong! (Instead of having to write hundreds of tests (and thousands of Node test runners).)
> :> and :<|>
You can just treat them as syntax, like the largest proportion of every other language, but with the opportunity of actually being able to write things like that yourself later.
Observe:
type API = "polls" :> Get '[JSON] [Poll]
:<|> "polls" :> Capture "question_id" Int :> Get '[JSON] Poll
:<|> "polls" :> Capture "question_id" Int :> "results" :> Get '[JSON] PollResults
The :> operator separates parts of a path, and the :<|> separates different URL patterns. This is the equivalent of this API from the Django documentation:
urlpatterns = [
# ex: /polls/
url(r'^$', views.index, name='index'),
# ex: /polls/5/
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
# ex: /polls/5/results/
url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results')
]
The only difference is it has less regexes in it, is capable of being checked for nonsense by a compiler much smarter than me, and gives you the aforementioned "server for free". I have had URL pattern-match errors with Django in the past, and having your compiler check that there aren't any is excellent.
Easier to maintain? Check.
Easier to read? Check. (If nothing, because of the lack of regexes.)
Defines the response type too? Check.
Easy to refactor? Check! Tired of typing "polls" at the beginning? Just lift it out: turn
type API = "polls" :> Get '[JSON] [Poll]
:<|> "polls" :> Capture "question_id" Int :> Get '[JSON] Poll
:<|> "polls" :> Capture "question_id" Int :> "results" :> Get '[JSON] PollResults
into
type API = "polls" :>
( Get '[JSON] [Poll]
:<|> Capture "question_id" Int :> Get '[JSON] Poll
:<|> Capture "question_id" Int :> "results" :> Get '[JSON] PollResults
)
Types are first-class :)
> How many PhDs does one require to understand/correct/debug all the :> and :<|> etc.?
Definitely less than it takes to become comfortable with the quirks of literally everything in JS: perhaps you should give something an honest shot before telling people who have derived real-world benefits from using it in production that it's useless?
Commenting on the edited comment :)
> Notice that your 5 - 10 lines of JS don't let you write code that works in any monad, whereas I can easily write
Maybe, maybe not. Depends on your requirements, really. The core language might never get this, but these 5-10 lines of code do some very important things:
- they explain monads faster and clearer than any of the countless monad tutorials that exist for Haskell
- they demystify monads and show that: hey, you've probably been writing monads all along (and re-implemented them yourself countless of times, no doubt)
- they (by necessity) dumb down the jargon-heavy lingo for easy consumption by average Joes like me :)
Edit: that page in particular has also shown me that I have used easily half of Haskell's things (functors of all flavors, monads, comonads, etc. etc. etc.) countless times over the years in Javascript and Erlang. I didn't even know I did, because no one scared me off with the theory, and strange explanations and names :)
Sure! I don't disagree: Haskell learning materials are a far cry from adequate, and we definitely need to learn from, e.g. the Rust/Elixir/Elm communities here. For now, this is worth trying:
http://haskellbook.com/ http://haskellbook.com/
Also, #haskell on IRC has been, without a doubt, one of the friendliest learning environments I've ever seen. Drop by sometime if the mood strikes you. :)
This is not really a definition of a Monad. For example, there's no mention of the Monad laws.
Monads are a very general and powerful abstraction that are not adequately described by your example. My advice to anyone is to read Phil Wadler's seminal paper, it is very easy to read.
Is it fair to say that your argument here is that "this resource was extremely valuable to me for understanding certain concepts in ways that Haskell-oriented resources in the past have not been"?
I think that's a totally fair criticism. I also believe that the Haskell resources can provide further value to you (and others in your position) over time if you choose to study them. Similarly, studying category theory or type theory or logic could.
Are these practical things to do? It depends upon your goals.
I don't think many people (and not the parent) "equate" FP with Haskell, but Haskell is in a way the most functional mainstream language because it is the only lazy one. You can have a purely functional language without laziness in principle, but, as Simon Peyton-Jones points has said, laziness, despite having serious costs, keeps a language designer honest by making it impossible to add side effects. The non-lazy languages all have side effects. A language like OCaml can still be relatively functional because of the way it is typically used. While JavaScript is not traditionally used in a functional way but can be.
http://www.cs.nott.ac.uk/~gmh/appsem-slides/peytonjones.ppt http://www.cs.nott.ac.uk/~gmh/appsem-slides/peytonjones.ppt
> Haskell is in a way the most functional mainstream language
1. Is it the most functional language because it's lazy? No
2. Is it the most mainstream language because it's lazy? No
3. Is it the most functional mainstream language because it's lazy? No + no = no
Laziness does not a functional language make.
> You can have a purely functional language
What would be the purpose of a pure FP? Oh. There would be no purpose.
> laziness ... keeps a language designer honest by making it impossible to add side effects
wat
Laziness is delayed execution. That's it. There's _nothing_ stopping you from delaying a side effect.
>Laziness is delayed execution. That's it. There's _nothing_ stopping you from delaying a side effect.
This is true, but unconstraimed side effects are too difficult to reason about in a lazy language to make them practical. So in practice, very few lazy languages have unconstrained side effects.
> Laziness is delayed execution. That's it. There's _nothing_ stopping you from delaying a side effect.
Laziness is about more than just side effects.
I think "evaluation" or "reduction" would be better words than "execution" here. Laziness (call by need) is an evaluation strategy for (beta-)reducing expressions, which has two nice properties:
- If an expression can be reduced without diverging by some evaluation strategy, then it can be reduced without diverging using call by need.
- Efficiency, in the sense that no duplicated work is performed.
The other common evaluation strategies are call by name and call by value. Call by name has the first property, but not the second; so there are cases when it's exponentially slower than call by need. Call by value has the second property, but not the first, so there are cases when it diverges unnecessarily.
This 'unnecessary divergence' is a major reason why most programming languages end up overly complicated to understand (at least, mathematically). For example, consider something like a pair `(cons x y)`, and its projection functions `car` and `cdr`. We might want to describe their behaviour like this:
∀x. ∀y. (car (cons x y)) = x
∀x. ∀y. (cdr (cons x y)) = y
This is perfectly correct if we're using call by name or call by need, but it's wrong if we're using call by value. Why? Because under call by value `(car (cons x y))` and `(cdr (cons x y))` will diverge if either `x` or `y` diverges. Since the right-hand-sides only contain one variable each, they don't care whether or not the other diverges.
This is why Haskell programs can focus on constructing and destructing data, whilst most other languages must concern themselves with control flow at every point (branching, looping, divergence, delaying, forcing, etc.).
I don't know Haskell but how is laziness related to FP?
The only way I can think to relate them is (a) FP tends to highlight the importance of value-semantics over all others, (b) non-termination is an effect, (c) FP also, subsequent to a, tends to emphasize control of side effects, (d) in a terminating lambda calculus all evaluation strategies are confluent/equal under the value-semantics, thus (e) laziness is particularly _available_ in a FP language.
Attempt at translation from CS-speak:
Laziness matters less in a FP language because if we consider non-termination an effect (impure), all pure functions should behave exactly the same regardless of whether they're "lazy" or not (plus in FP sameness is defined as same values, due to "value-semantics" - unlike OO where every object has a unique identity, different from all others) because in the absence of side-effects order of evaluation is not important.
Of course reality is different, and laziness has very visible and important effects when Haskell programs run on today's computers :)
You’re mixing up laziness and purity. Purity makes side effects impossible, so to speak, not laziness.
Haskell is the most functional language because a Haskell function is a mathematical function, which can only transform its arguments into a value. Everything is a constant in Haskell, and functions transform one or more constants into a single, new constant.
> Basically this is (in my mind) what's wrong with Haskell: it's overly concerned with the Platonic ideal.
I read this and I think what's got you rustled here is that Monad is such a generic concept. It's quite higher level and so you can do novel things like write functions that don't know how they're executing, just that they are.
As an example:
-- Config is a typeclass that enables getting a
-- keyval from a config. The return type is MonadIO
-- because config might need IO.
loadTarget :: (MonadIO m, Config a) => a -> m a
loadTarget config = do
v <- grabKeyVal "host" config
w <- grabKeyVal "port" config
return (v,w)
What does that code do? The answer (if it's written carefully) is that it depends on what the underlying monad is! And that's a good thing, in many cases. If the monad is Maybe + IO, then you have a conditional loader.
But if the monad is an array and IO then you can specify many hosts and many ports and this code enumerates them all. If that's passed to a ping function like so:
loadTarget config >>= pingHostPort
-- alternatively
doPings config = do
hostPort <- loadTarget config
pingHostPort hostPort
Well then your code will do the right thing, but a different thing, based entirely on the types alone! And you can generalize this out to even more powerful types. For example, you could write a web app that server side could do local network ports for you (why? you're a maliclious hacker of course!). In that case it might make sense to use the Continuation monad.
tl;dr and finally:
You say this is stupid abstract stuff, but the folks delivering features to you in the Javascript world disagree. You have generators now, which are a much more real and fair explanation of how to model monads in Javascript than that silly code snippet you posted that doesn't capture the spirit of them at all.
What's more, careful application of these concepts leads to libraries which are just better than anything you can have without appealing to generators. A great example of this Purescript-config. Here is an actual (redacted) same of some code I use at work in an AWS Lambda function to read the environment:
https://gist.github.com/KirinDave/9af0fc90d005164743198692f380d4d9 https://gist.github.com/KirinDave/9af0fc90d005164743198692f3...
So I have complete error reporting (full sets of missing keys) just by describing how to fetch the values from the environment. I can transparently switch to any other file type under the covers by changing from using "fromEnv". I only know that there is a key-value store in there.
Doing this in OO is really, really hard to get right, because imperative OO code cannot easily parameterize the execution strategy hierarchically without appealing to generics and ad-hoc polymorphism. That's hard.
The applicative style adopted here is very simple to re-interpret, because we can parameterize code on monads and applicatives (which explain the execution strategy of this code beyond its coarse structure) as we see fit.
You can do that with generators but it's frustratingly hard. Doing it in a truly generic way? Even harder. For this approach, the results are free (and Free, but hey).
I can give you other examples of how Purescript makes certain difficult aspects of javascript simply vanish, if you'd like.
Thank you for examples.
I have one complaint though:
> Doing this in OO is really, really hard to get right,
Why do you equate FP with monads? Moreover, why do you equate FP with Haskell/Purescript (statically typed FP with monads)?
The use of monads is a side-effect (ha!) of committing to purity throughout a language, and that's what FP is being equated to: pure statically-typed FP.
(You can argue about how justified that is, of course. I'm not going into that, but you might want to see what John Carmack has to say[0]. No, he doesn't end by saying "we should all convert to the church of Haskell now", but he does talk about how large-scale game programming refactors are made easier when you're working with no (or very disciplined) side effects.)
Monads are not the only way to deal with effects while keeping purity, although they were the first discovered and so on: algebraic effect systems as in Koka[1] (and as simulated in Idris or PureScript) are another alternative. Koka infers effects, so it's probably easier for a C-family programmer to pick up (I know little about it, though).
[0]: https://www.youtube.com/watch?v=1PhArSujR_A https://www.youtube.com/watch?v=1PhArSujR_A
[1]: https://www.microsoft.com/en-us/research/project/koka/ https://www.microsoft.com/en-us/research/project/koka/
Indeed Haskell the language doesn't do any IO. Instead, it creates a tuple containing
* a description of the action to execute (e.g. read line from STDIN)
* a function that will take the result of the previous action (e.g. the line) and return the next action to execute (e.g. writeLine)
[readLine, \line -> writeLine(line)]
This main action is just a description, the runtime itself takes it, does the actual IO described in the first part of this tuple, evaluates the second (function) part of the tuple with the result of that IO, receives a new pair of action/function, does the action's IO part, evaluates the new function and so on until it gets a nil (end of the "linked list" so to speak)
You could imagine this as a linked list of actions, where the "link" is actually a pure function you call with the result of execution the first part to get the rest of the list (or nil to terminate). This is still pure because the action itself doesn't do anything. If you return an action from a function, it doesn't actually execute, its just a value to be interpreted.
Does that make a real difference or is it just theoretical "purity"? Yes it does make a difference.
For example, if you create two action values, you haven't actually run anything yet, just created descriptions of actions. You could put them in a list and create an action description that will tell the runtime to run them in parallel, for example using: https://hackage.haskell.org/package/parallel-io-0.3.3/docs/Control-Concurrent-ParallelIO-Global.html https://hackage.haskell.org/package/parallel-io-0.3.3/docs/C... ... or in sequence https://hackage.haskell.org/package/base-4.10.0.0/docs/Prelude.html#v:sequence https://hackage.haskell.org/package/base-4.10.0.0/docs/Prelu...
For refactoring, it means that you can still take functions that return actions and substitute them with their values, e.g. if you have
readItems 0 = return []
readItems n = do
x <- readLine
y <- readItems (n - 1)
x :: y
main :: IO
items <- readItems 3
putStrLn "The first item is " ++ (head items)
you could pull (readItems 3) out of main!
read3Items = readItems 3
main :: IO
items <- read3Items
putStrLn ...
and everything is exactly the same, since all you've pulled out is a description of an action. Equational reasoning (you can substitute an equation with its result) still works - which is great for refactoring!
> Servant isn't the "simplest" way to get BasicAuth: you get what you pay for.
But that's not what I complained about ;)
It's funny how in a separate thread someone insists that I have to understand the whole concept of a monad, its three laws, read Wadler's paper etc.
In his thread however, it boils down to: oh, just memorise these lines of code, and just blindly copy-paste them wherever.
So, my complaint is: to actually understand what's going in the "simplest way to do BasicAuth" I need a whole lot more than just look at the code.
I need to understand why I need no less than ten (!) language extensions before I even begin to implement BasicAuth. And five of them just to workaround some Haskell limitation based on some report from 1998 (I'm guessing)? What happens when I move on to OAuth? What will I need then?
Also, "oh, you don't really need PhDs in type theory to use Servant" slowly descends to "oh, here are two things regarding types that you'll have to learn". On of them has no useable documentation except some comment on StackOverflow. The other one requires you to be well-versed in type theory.
Hence my complaint about "you need a PhD in type theory to work with Haskell".