Read this if you don't follow: "In Rust, ordinary vectors are values" http://smallcultfollowing.com/babysteps/blog/2018/02/01/in-r...
Read this if you don't follow: "In Rust, ordinary vectors are values" http://smallcultfollowing.com/babysteps/blog/2018/02/01/in-r...
I almost think of Rust as a lower-level OCaml (faster and better support for memory management, but with somewhat weaker support for typeclasses and an inconvenient ADT syntax). An ML language you can conceivably write an OS in, but I would personally be nervous to write a complex finance application vs. OCaml.
And of course Rust also has typeclasses, with a design strongly influenced by (but somewhat weaker than) Haskell. Monads and Readers are all over the shop in modern Rust precisely because they are so useful in modern Haskell.
Basically, the reason why Rust is such a good low-level programming language is the reason that functional programming matters.
OCaml is a garbage collected and mono-thread language.
You obviously have the same guarantees in that you can't do what would be problematic.
As a side note, a lot of the people currently writing Rust would ihmo be better served by Ocaml. You rarely need the protections Rust provides and they have a real complexity cost.
Well, yes, but that's the point. Rust gives you the same guarantees without limiting you to using a single thread or requiring you to use garbage collection.
>You rarely need the protections Rust provides and they have a real complexity cost.
Agreed.
> ...higher-order functions and lazy evaluation, can contribute significantly to modularity.
You can not have lazy evaluation without referential transparency, and you can not get referential transparency with mutable variables.
Rust does good use of high-order functions, but gets nothing near the modularity of Haskell code.
This has not been my experience.
After years of doing pure functional programming (in Elm) professionally, I was surprised how much Rust felt like writing in an ML family language.
I still prefer referential transparency (though I don't think it would have been the right design for Rust), but "nothing near" does not fit my experience. I'd say the ergonomics of borrow checking versus a GC (which you don't have to think about) is the bigger gap.
John Hughes is a big believer in the value of laziness. There are many of us who believe laziness turned out to be a dead end; it makes some code nicer/more elegant, makes performance optimization much harder, and brings space leaks into your life. Are those costs worth the benefits? Not even close to worth it, if you ask me. I'll gladly take the referential transparency and pass on the laziness.
Almost all the researchers who started working on Haskell specifically because they wanted to explore the power of laziness...ended up pivoting to research type theory instead. I don't think that's a coincidence.
I haven't used Rust heavily enough to comment on how it compares in great detail, but comparing to Elm as a proxy for Haskell doesn't really work.
Frankly, paradigms are a really lousy way to think about languages. I wrote a series of blog posts about this[1], but this opening lecture from one of Brown's PL courses I think does a better job of making the point:
https://www.youtube.com/watch?v=3N__tvmZrzc
It's useful to talk about what say, GC, laziness, lifetimes, ownership, typeclasses/traits, higher-kinded types, higher rank types, variants, elm-style records, etc. do to a language, and how they compose, but I think you can't go very far talking about how "paradigms" compare.
[1]: https://zenhack.net/2018/07/14/three-funerals-in-the-name-of...
Sure. My experience has been:
* GC, lifetimes, and ownership are all high-benefit and high-cost. The cost with GC is at runtime (where the cost is so high that in many domains GC is not tolerated at all; in many others, of course, we take it for granted as fine), and the high costs of lifetimes and ownership are at development time.
* Variants and records are high-benefit, low-cost.
* Higher-rank types are low-cost, low-benefit.
* Laziness and higher-kinded types are both features with costs that significantly outweigh their benefits.
It sounds like you disagree with the last bullet point. If so, then either we've had different experiences or we walked away with different conclusions from them.
> It sounds like you disagree with the last bullet point. If so, then either we've had different experiences or we walked away with different conclusions from them.
I more or less agree on laziness (at least lazy-by-default; having a lazy type as found in OCaml available is a big win for little downside).
Re: Higher-kinded types: I'm curious as to what you think the high costs are? My impression is that they've mostly been left out of Elm due to pedagogical concerns. Is it just that or are there other things?
Another cost is in standard library complexity. You can't have HKP and not have a stdlib with Functor/Monoid/Monad etc. As Scala has demonstrated, if you have HKP but don't put these in the stdlib, a large faction will emerge pushing an alternative stdlib that has them. A larger, more complex stdlib is a cost, and so is a fractured community and ecosystem; HKP means you'll have to pick one of those two.
API design is another. Without HKP you write a function that takes a List. With HKP you now need to decide: should it actually take a List, or is it better to take a Functor/Semigroup/Monoid/Applicative/Monad instead?
If you choose one of the more generic ones, now it takes more mental steps to collapse the indirection when reading it. (1. I have a Maybe Int. 2. This function expects an `s`. 3. `s` is constrained by `Semigroup s`. 4. Can I pass a Maybe Int to something expecting a Semigroup? Compare to "This function takes a `Maybe a`", and multiply that small delta of effort by a massive coefficient; this is something everyone who reads these types will do many, many times.)
This indirection also has implementation costs; in theory you could make docs and error messages about as nice if HKP is involved as if not, but there's an implementation cost there, and it seems like it must be pretty steep if you stack up languages with HKP and their quality of error messages and docs against other typed languages that don't.
So I'd say it's one huge cost (automatic induction into the highest tier of learning curve steepness), one big cost (either a larger and more complex stdlib or fractured community), and several smaller costs with high coefficients because they come up extremely often.
Yeah there are benefits too, but I don't think they get anywhere near outweighing the costs.
I think that you overstate the cognitive overhead of reading polymorphic type signatures to those who are reasonably familiar with common idioms. Taking a second to remember that `Maybe a` is a semigroup if its argument is seems like a small cost to pay to me.
I think there's a significant benefit to highly polymorphic functions in the standard library which seems to rarely be brought up. Polymorphic functions are applicable more often. So then if the function is already written for you, and you use it, then anyone who reads your code has to look at one less definition to understand it (if they're familiar with the library function). This forms a larger common vocabulary and in some ways imposes a smaller cognitive overhead on the reader.
Speaking more broadly, people are often frustrated by the number of abstractions from the standard library that they have to learn to be productive. But they don't notice all of the abstractions they now don't have to learn in individual codebases - because they don't have to exist. And this effect adds up - every time there would have been a slightly less well implemented, proprietary, monomorphic version of a function in a codebase and you use the polymorphic standard library one instead, everyone who reads that code has one less thing to get their head around. It pays for itself.
I also feel like you understate the benefits - the amount of times I think for 20 seconds and realize that the complicated function I was about to write is just like, `traverse` or something is incredible to me.
I think it's possible to to have legitimate disagreements here, which are often driven by differing personal experiences, and by the kinds of domains people are operating in and so on. I don't think there's a one size fits all answer.
> HKP is practically unavoidable on the road to understanding a Haskell program that prints Hello World.
main :: IO ()
main = putStrLn "Hello World"
Doesn't require an understanding of HKP at all. There isn't even any LKP. I do agree that it's necessary for a productive employed Haskeller, but not a beginner playing around with simple command line apps. There's a distinction to be made between understanding enough to make it run (Just label the IO bits with IO and think of 'do' as kind of like imperative programming but not really) and understanding more deeply, which only becomes necessary later.> With HKP you now need to decide: should it actually take a List, or is it better to take a Functor/Semigroup/Monoid/Applicative/Monad instead?
It doesn't seem like a decision that would involves a lot of cognitive overhead. In general I'd probably just go with whatever type GHC infers. Failing that, it kind of arises naturally from like, what the function is about. Is it about reducing the List to a single value? Use Foldable. Is it about transforming the elements of the list? Use Functor. Is it about nondeterminism, but the code isn't necessarily specific to that computational context? Use Applicative if there are no sequential dependencies (which the type checker knows anyway) and Monad otherwise. Ok, maybe it seems a little complex when you write it out, but it's really fairly instinctual.
Semigroup doesn't involve higher kinds at all. What you seem to be discussing is either type classes or just a pile of junk from abstract algebra. Fwiw, Elm has semigroup too -- it's called appendable (which is a much much better name...).
> Learning curve is a very high cost by itself; HKP is the reason Haskell is notoriously difficult to learn.
I don't think any one feature of Haskell is why it's hard to learn. I think the reasons are much more mundane, the main ones being:
1. The language is just enormous. It's a lot to need to have in your head to understand some bit of code you come across. Folks end up picking a (small) subset of it just to stay sane, but this doesn't help you when trying to come across a new library; you basically need to have most of the language in your mind somewhere to understand $RANDOM_NEW_LIBRARY reliably. And because it's an issue of sheer size, there's no short-cutting it. It has a lot of features with heavy overlap in use cases, so you spend a lot of time thinking about silly things like "Should I use FunctionalDependencies or TypeFamilies?" "I'm writing a library that needs to generate a bunch of boilerplate code, should I use GHC.Generics, TemplateHaskell, or something else?".
2. The community is really lousy about pedagogy. They tend to lead with the abstraction, which is just not how people learn. I really wish this[1] had been written like a year earlier; it would have saved me a lot of trouble wading through useless instructional material trying to learn this stuff. It doesn't seem like the bulk of the community took that to heart though, and while there are some good learning resources out there, there's a sea of worse-than-useless ones.
3. There's a culture of complexity/over-engineering. I don't think this is unavoidable, but it's particularly a hazard of being research language where to a large extent the whole point is to play with crazy ideas. The maintainers still see the language as primarily a platform for experimentation, so KISS can be a hard thing to push for.
> and it seems like it must be pretty steep if you stack up languages with HKP and their quality of error messages and docs against other typed languages that don't.
I'm not really sure I buy this; I think the list of languages that have these things and have seriously made good error messages a priority is pretty short (empty?). I can point to some simpler ML dialects that still have some really lousy error messages.
[1]: https://byorgey.wordpress.com/2009/01/12/abstraction-intuiti...
Sure you can: https://homepages.inf.ed.ac.uk/wadler/topics/linear-logic.ht...
Rust's ownership types and lifetimes actually allow for this just fine. The latter is the essence of how Haskell's ST Monad works; you can use lifetimes to get locally-mutable state without violating global invariants, since once they go out of scope they can't be reused.
It's interesting to observe that, without "magic" standard library functions and `unsafe`, Rust's type system actually completely constrains mutability, and if a function doesn't have `mut` somewhere in its type signature, it doesn't break referential transparency.
That said, in practice, the language does have magic functions that violate this property, and they do so in a way that means you can't use the above reasoning principle at all. Also, mutability being constrained by the types is not the same thing as typical code not using it everywhere, which is the situation with rust-as-found.
"… allowed to update the unique object destructively without any consequences for referential transparency."
Uniqueness Typing
https://clean.cs.ru.nl/download/html_report/CleanRep.2.2_11....
Immutable datastructures in functional languages are not datastructures that cannot be mutated, they are datastructure that efficiently produces a new, updated, version on each operation (such as the cons list). While they are slower than traditional datastructure for a typical use case, they tend to be a lot easier to think about, make parallel code much easier and let you easily get back in time to previous versions of your datastructures (when discovering rust, I was actually disapointed that they were not available in the std).
The im[0] crate offers what seem to be good Rust implementations of that kind of datastructures (with abetter explanaition of why you would want them).
This claim seems dubious. But in any case, I think the OP was just talking about immutable data, i.e., in Rust terms, data to which there is no mutable reference. The data in question could be a simple integer.
As a matter of fact I have seen data structures, in FP language, called immutable while having operations to mutate them in place.
I believe the OP spoke of both Rust immutable data and the fact that most FP languages have only immutable datastructures (different mecanism to deal with similar problems).