Hacker News new | past | comments | ask | show | jobs | submit login
Haskell Fan Site (stanford.edu)
253 points by allenleein on June 23, 2019 | hide | past | favorite | 163 comments



Ben Lynn is a winner of the 26th IOCCC https://ioccc.org/2019/whowon.html with a tiny Haskell Compiler. We will be releasing the code in early July.


He's also the L in BLS Signatures and is currently at DFINITY building their system using both BLS and Haskell.

https://dfinity.org/team/


From my experience Haskellers spend more time talking about how perfect and pure Haskell is than building real-world applications.


Probably because real-world applications mostly aren't that interesting -- they're assuming you're doing that stuff well on your own. You could write "real world applications" in any language you want, some faster (to develop) and others more efficient (at runtime) than haskell, but that's not what haskell brings to the table.

Haskellers (well me) view Haskell as introducing reasoned and disciplined immutability, purity, type safety, etc to the mundane world of real-world applications, which is why Haskellers get excited when these benefits can be applied. Writing a database query? boring. Writing a database query that is guaranteed at compile time to contain the right columns to build the right domain object such that you can never write an incorrect one? that's interesting.


I hear you. Other languages have those properties. Including TypeScript.

In my experience I've found real-world applications, by definition are never as pure and perfect as we like them to be. Users do stupid things, or vendor APIs don't quite fit what we want.

And personally it's why I think the Haskellers I know often get stuck in the mud. Seems the language, or just the Haskellers I know, are better suited for a academic applications.

I'd love to be proven wrong as I'm into type safety and FP, in general.


I absolutely love TypeScript -- right now Node + TypeScript is my pick for the interpreted language wars (Node/Python/Ruby/Perl/etc).

> In my experience I've found real-world applications, by definition are never as pure and perfect as we like them to be. Users do stupid things, or vendor APIs don't quite fit what we want.

This is actually one of haskell's greatest strengths IMO -- it enforces the kind of discipline that sidesteps this problem completely. The kind of functions you write with haskell don't allow bullshit -- non-nullable types are the biggest example of this I can think of, there's also the pervasive use of `Maybe Value` and `Either SomeError Value` types.

In other places it's usually referred to as Domain Driven Design or the onion architecture -- but best practices for software development usually dictate that you get rid of bullshit as early as possible on the borders of your application. Put simply, don't let invalid/incorrect input make it into your system.

> And personally it's why I think the Haskellers I know often get stuck in the mud. Seems the language, or just the Haskellers I know, are better suited for a academic applications.

Totally true, we do get stuck in the mud, but it's such nice mud -- the things you worry about are just different (and I think this is what you're seeing). Roughly zero haskellers are worrying about NPEs -- there's no urge to get something to "just work" because "better" is right there, and haskell encourages you to strive for it.

That said, it's absolutely the case that a ton of energy in haskell land is spent on academic pursuits, but that's actually good imo, languages that don't do this get stale.

> I'd love to be proven wrong as I'm into type safety and FP, in general.

Well I don't know that this is something someone else can prove to you, outside of people just writing more middling/regular software in haskell and getting it out there, which is a bit of a community thing. I do my best to write practical haskell software and write about it, but a bunch of my projects aren't open source (just yet -- I'm heavily considering making one of them open source right now). Rust is also proving to be a huge (welcome) distraction because it gives much of the haskell creature comforts with within-C++/C range efficiency.

Maybe take haskell for a spin? The learning curve is high but it will bend your mind in a good way. Or if you're of the web persuasion, try Elm/Purescript, they're thoroughly practical.


The refreshing part about Haskell to me is that it is declarative. It feels more natural to describe what you want to happen than to try to think like a machine and compose sequential machine instructions for it each of which can fail at any execution.

Haskell's compiler is incredibly strict. It'll guide you until the code is near bullet proof. The outcome is that the surface area where things can go wrong is relatively a lot smaller than in just about any other language our there.


Haskell is not declarative. It is a specific form of lazy + graph-reductive. Thinking of it as declarative leads to polynomial exponential runtime cost in CPU/memory and not understanding why or how to fix it.

https://stackoverflow.com/questions/40130014/why-is-haskell-...


I'm not talking about the compiler or the evaluation. From programmer's perspective the language does seem fairly declarative to me, mostly consisting of expressions instead of statements.

I don't know what's wrong about thinking about expressions as declarative which in my opinion they are. How would thinking about expressions imperatively help me avoid those exponential runtime costs?

For example consider the list comprehension: [toUpper c | c <- s]

If you compare this with your typical imperative for loop to construct the same data structure I find this declarative.


Thanks. You in the Bay by chance?


Sorry super late but no, I'm in Tokyo actually (was in Austin before that) --feel free to reach out (email in bio) if you ever want to chat though!


> I hear you. Other languages have those properties. Including TypeScript.

Limited nominal typing, limited support for record-like functionality (ability to handle datastructures generically-but-safely), no HKT (so difficult to handle secondary concerns in the type system - e.g. writing a function whose type enforces that it's called within a database transaction, but you can still compose it with other such functions and run them all together in a single transaction). I wanted to like TypeScript, I really did, but after a month or so I was fed up enough to actually put nonzero effort into building with Scala.js (which turned out to be really easy) and within an hour I had the safety properties I was used to and was more productive as a result. (Scala isn't Haskell but the advantages are similar).


Typescript's type system is unsound by design. And it doesn't have the sophistication of Haskell. So it isn't that similar IMO


??? Could you drop some links/justification? I know I learned a lot from the set of slides about Flow vs Typescript[0].

Also, not having the sophistication of Haskell doesn't make something bad -- Rust's type system doesn't have the sophistication of Haskell and I think it's a fantastic language (I struggle not to pick it over haskell most of the time).

Typescript, sound type system or not, has brought many of the benefits of the Haskell ecosystem to JS. Python, Ruby (and Perl?) are following in the footsteps right now with their gradual typing schemes. AFAIK JS was the first to get something like this so right -- going from syntax sugar to actually highly beneficial type checking. The stuff people would put on top of C to make it safer stands out but I can't remember such transpiling ever being so embraced and beneficial to a language.

[EDIT] - after some searching, maybe you're referring to this issue (amongst others): https://github.com/Microsoft/TypeScript/issues/9825

[0]: https://djcordhose.github.io/flow-vs-typescript/flow-typescr...


Which doesn't necessarily mean it's not useful for building real world things. Lumi [1][2] was built with Haskell (and PureScript) and looks like a great product.

To me it just means more people need to a) build more real things in Haskell b) write about their experience and help advance the state for non-academic newbies and professionals c) avoid the weeds of the language theory stuff and stick the simple established stuff to stay productive (which does exist).

You could also make the same argument for JavaScript. So many people are trying to reinvent the wheel every other week in the frontend world, it's just as easy to get distracted by the language/framework noise.

Just be a mature developer and don't get suckered into the latest shiny objects.

[1] https://www.lumi.com/

[2] https://www.lumi.dev/blog/purescript-and-haskell-at-lumi


Don't forget: XMonad, pandoc, and ShellCheck


Correct. Interesting you bring up JS, as the Haskellers I spoke of loathed JS and treated it and its developers as below them. But that's just my experience.

I hope Lumi was able to gain efficiencies using PureScript and Haskell. And that they're able to attract talent. Thanks for sharing.


In my experience Haskellers speak that way about every language that isn't Haskell or Rust, which I found to be probably the biggest turnoff for me wrt learning Haskell.


There was an article posted somewhere about why Haskell isn't used very much in the data science/ML community, and the feedback was that the tooling is mostly not so good. It was kind of amusing to read all of the comments on /r/haskell saying "no it's good." I don't think Haskellers would really understand, but lots of people (especially data scientists in my experience) view languages only as tools. The Haskell community seems to derive lots of value from spending tremendous time basking in the elegance of their tool, rather than actually using it to solve problems. So /r/haskell will continue basking, the rest of the world will continue not using it to solve problems.

Or an extremely long winded "yeah I have the same experience as you."


I think the Haskell people saying the tools are good (like me!) are just using a very different criteria of "good" than you (and most data scientists!).

For me a "good tool" is robust, reliable, and easy to fix. For most of the data scientists I know, a "good tool" is one where "a single easy to remember command magically does the thing I want in one go".

And I get it, that last part is super appealing, especially for people who care more about answers to their problem than in technology, I don't blame people for wanting that.

But my personal experience is that many of these magical single command tools break a lot when you try to use them in any non-standard environment (for example, something that is not Ubuntu and where you don't have sudo to fix/install system packages) and I work in environments like that a lot. So the fact that e.g. cabal-install requires a bit more explicit work to setup initially is outweighed by the fact that I can reliably install it on any *nix system where I have a login and sufficient diskspace and have it Just Work.


> I think the Haskell people saying the tools are good (like me!) are just using a very different criteria of "good" than you (and most data scientists!).

Exactly, which is why Haskellers should listen to and respond to feedback rather than asserting it is the feedback-giver that is wrong.

> For me a "good tool" is robust, reliable, and easy to fix. For most of the data scientists I know, a "good tool" is one where "a single easy to remember command magically does the thing I want in one go".

I don't agree with this statement and I think demonstrates some of why Haskellers are kind of frustrating. Most data scientist (and most people in general) want to focus on solving the problem they're tasked with, not elegantly and efficiently positioning themselves to be able to do so. Jupyter and Numpy/Pandas are great because they allow the user to focus exclusively on solving their problem, not on language or framework-level concerns. This is not "a magic command that does the thing I want in one go." It is the separating the needs of the hammer maker from the needs of the carpenter.


I use it to solve problems. I have a database with millions of unstructured JSON documents. I wrote a tool in Haskell to scan the database, parse the unstructured documents and collect the results. It displays the ratio of successful parses and the top N parse errors with a sample to add to my test suite.

Once I can parse 100% of the database I can use another library I'm working on that can migrate between data structures while preserving information and provenance.

Then I can safely migrate millions of lines of unstructured documents with dozens of weird corner cases to a collection of documents with consistent structure and few corner cases.

Sure I could do this in pretty much any language on the market but I've put relatively little effort into this and am nearly done. Programming in Haskell has a good power-to-weight ratio.

I don't really have anything to complain about tools-wise. It's all standard fare or better than most other language ecosystems as far as I'm concerned.


We're building https://hercules-ci.com/ and not talking much about purity :)


We need more startup to use Haskell in production, like Dfinity.


Mercury.co uses Haskell. It’s been great so far.

Mercury.co/jobs if you are interested.


Co–Star is hiring!

costarastrology.com/jobs


Has Dfinity launched yet?


No


> Programs should be easy to express.

in my experience, languages that make it easy to express small programs tend to be counterproductive for large systems with many modules, many people working on the system and the desire for fine-grained control.


Sounds like you perhaps have experience of dynamically-typed languages? Haskell is statically typed, compiled, has a scalable multi-threaded runtime, a very good FFI; and was designed around the notion that large programs should be built by composing small programs.


I tend to prefer eager ML languages vs Haskell. Some lazy abstractions are a bit leaky. Performance might degrade and it's really hard to debug.

Laziness is nice, but not by default perhaps.


It's not difficult to enforce strictness in Haskell in the places where it matters.


Globalized type inferrence is still a problem when you want to build large programs. You change the implementation of one function and 20 functions further there is a problem and you need to figure out all the interactions on the path between. Rust strikes a great compromise here IMO.


I think that these days the Haskell best practices recommend adding a type annotation to all toplevel declarations, to help keep error messages manageable.


Sort of sounds like the interfaces/components/boundaries/separation of concerns in your modules isn't up to scratch. This is more of an org issue around management as opposed to the language unless you think rust has a set of fetaurs that help with this


I neither want to deny or confirm this but if love to see a more in depth analysis of the idea.

Why? Is it because of the "fine grained control" part? Because it breaks the neat abstraction?


Expressiveness is too powerful. I have to understand what the author was thinking. What you typically want is composition. With composition, you have to understand what the author is doing.

Painting with acrylics vs Lego.


fair point, expressiveness shifts idioms toward "single man private jokes"

A point I just realize the value.


But you could just have expressiveness through composition then?


It's basically the same reason Go was created: not many advanced features, lack of abstraction power, leading to highly predictable codebases so that companies can easily onboard new software engineers.


This was probably the theory behind their decisions. While I agree that the lack of expressiveness might discourage some incidental complexity, for example premature or unecessary abstractions, the inherent complexity of a problem will remain. Removing expressive power, in general, will lead to ad-hoc and cumbersome workarounds. For example, look what the Kubenetes codebase had to do to work around the lack of generics: https://medium.com/@arschles/go-experience-report-generics-i...


Haskell is perfect for that world you describe. Thanks to purity and strong types, Haskell modules compose better than modules in other PLs.


> I have seen languages that practically force the programmer to run a heavyweight specialized IDE, and that require 14 words to print 2 words.

I picked up Haskell again last week after a long hiatus and although one does not need a heavyweight IDE, the build system still felt like a bit of a mess. After adding `parsec` (a popular library for parsing) to a stack project, my machine spent over an hour compiling dependencies at which I point I gave up.


Try using nix instead. Aside from being a fantastic package manager for many languages it’s a godsend for Haskell. The lengthy compilation times with Haskell are one of the not so wonderful parts of the language, but they largely disappear with nix, and the Haskell ecosystem on nix is very well supported, with a large community. Honestly I’ve been developing Haskell for years and have never once used stack. I did use cabal before I discovered nix, but since then I’d hardly dream of using anything else.


Thanks for mentioning this. It's a bit hard to get started as the signal:noise ratio is pretty bad. Not because there's bad info out there, per se, but there's a lot of _old_ information out there and it feels hard to (a) stay up to date with what's best and (b) it _seems_ like stuff is still being figured out.

The success of Python, I think, is largely due to pip; the success of Ruby due to bundler; the continued relative success of Java due to maven, etc. And it seems to me Haskell is still figuring this out.


Funny,I tried two years ago to use Nix to have a new onegcc on an old Linux OS, I didn't succeed and finally recompiled gcc from the sources which I found much easier (and better documented) than using Nix.


I didn't know about this, glad I read your comment. Stack/cabal is painful.


For Nix and Haskell in production, I highly recommend this resource:

https://github.com/Gabriel439/haskell-nix

More free resources about learning Haskell:

1. https://github.com/allenleein/brains/blob/master/Zen-of-Func...

2. https://github.com/allenleein/brains/tree/gh-pages/Zen-of-Fu...


Thanks for introducing these. But a question I've been asking is what's the optimum way to start learning Haskell? There're tens of books in the links you mentioned, mostly ranging >400 pages. Obviously it's not efficient to read them all and then decide which was the best. On the other hand, with Haskell evolving all the time, I worry if I start a book I might miss out on some other stuff covered in other books.


Please start from this book:

Haskell Programming From First Principle (The most popular in community)

https://github.com/allenleein/brains/blob/master/Zen-of-Func...


Thanks! I've heard about the authors. Just out of curiosity, how does Learn You a Haskell for Great Good stack up against other resources? (heard a great deal about it here and there.)


Learn You a Haskell for Great Good ends up being a taste thing for me. I didn't really enjoy it and preferred things that stayed in the pure areas of the language longer and tried to demonstrate more abstract math principles using Haskell as a language rather than CS focused data structures and manipulations (I found a discrete logic preprint book from somewhere that was much more enjoyable). Real World Haskell at the time was then a decent reference to trying to do real things.

I would certainly try it but it works kind of the same as gotour: if you want a worked example of a characteristic of the language for a specific thing it's great, if you are trying to get your hands around the language by working through it 'cover to cover' in a sense I felt like I wasn't achieving the level of understanding I hoped for.


I also saw "Beginning Haskell: A project-based approach" by Mena which even talks about lenses. As my primary use-case from Haskell would be in data science, I think this book might be useful.


LYAH is a good overview of the basics of the language, but it provides little in terms of motivation and exercises. Then again, I learned haskell in it, and it's now my main (non-work) programming language, so it is possible to learn it for great good.


how does nix eliminate compile times???


It doesn’t eliminate them entirely; it’s just very smart about allowing packages to be built exactly once, and having a deterministic output such that it can be determined before a package is built whether a prebuilt version is available. This prebuilt version can either be the result of a previous build on your machine, or on another machine served from a repository.

Of course, there are times when you need to compile yourself, but most slow-to-compile packages, such as Aeson, Lens, Servant or the aforementioned Parsec, and many more on top of that have prebuilt binaries available when built from community snapshots (nixpkgs/nixos). You can even pin your package definitions to guarantee that you’re building something that will have prebuilt versions available. New project build is usually less than a minute or two, sometimes substantially so (anecdotally). Again, not always, but usually.


Looks like it offers precompiled packages:

> Nix lets you download precompiled Hackage packages whereas stack compiles them on your computer the first time you depend on them

Source: https://github.com/Gabriel439/haskell-nix#background


> Writing code should be comparable to writing prose. A plain text editor with 80-character columns should suffice. Coding should feel like writing an email or a novel. If instead it feels like filling out a tax return, then the language is poorly designed.

So this page has nice statements like those ones, but no justifications. Why should writing code be comparable to writing prose or writing a novel? I don't see why that would result in a better implementation.

Prose is full of obscure and non-well defined rules that often require subjective judgement. That's something I want to avoid when writing programs. Program source codes are better written as clear and unambiguous as possible, limiting potential misunderstanding whenever possible, where a person writing prose will play with rhythm, symbolism, rhymes, atmosphere development, etc and can use potential misunderstanding and multiple meanings to add depth to their work.

Filling a tax return seems to be better IMHO, I don't need to develop my own style over years and years of writing, I learn the (quite strict) rules, apply them, anyone who also knows the rules can easily fix/extend/improve what I've done.


Surely the point there is that you should feel like you can write your idea down any way you want, instead of being bound by an extremely tightly constrained form that only lets you put very specific things in certain places. Your tool is a blank sheet of paper and a pen; it's not in your way at all.

"This year was a good year: I made $100,000 at my job although I did lose $500 in the stock market."


Given the laziness, you'd think memoizing an arbitrary pure function would be easy in Haskell - at least as easy as Python, where you can do it with a one line decorator.

But no, it's pretty complicated: https://wiki.haskell.org/Memoization you even have to involve fixed point combinators. Pretty disappointing.


Don't you just call a library function? https://hackage.haskell.org/package/memoize-0.8.1/docs/Data-...

Just like in Python...?


"However, it appears to work."

Wow. I don't remember reading such blasphemy in other Haskell docs. And I like it, no joke. Though it's likely about the performance characteristics; memoize probably behaves (and not just appears to behave) like identity modulo performance, that's just not documented clearly enough.


I love FP and have programmed Haskell in professional contexts. But I have to admit, I don’t like it and it’s strong formalisms make so many problems much harder than they have to be.

Take lenses for example. All of this unnecessarily complicated shit because Haskell doesn’t have any reasonable record syntax.

I appreciate Haskell as a research project and it’s clearly pioneered many important FP concepts: monads, free monads, trampolines, etc. But I would never ever choose it for a new project. OCaml, F#, and Scala would be my go to. Nice FP, but not so opinionated to cripple/slow you down when you want to dip your toes in mutation. And in my experience, almost all non-trivial programs require at least a little bit of mutation. And when it’s required, I really don’t want to waste my time with IORefs or STRefs or whatever.


>Take lenses for example. All of this unnecessarily complicated shit because Haskell doesn’t have any reasonable record syntax.

Lenses (in the original, pre-Laarhoven form) are literally the same as properties in, say, C#. The extra complexity in their modern form is to pack the setter and getter into a single function, and allow in-place modification (without going throgh getter and then back through setter). The extra-extra complexity in the lens package is the result of Edward Kmett et al generalizing it to support traversals and pattern matching, while also providing support for the entire standard library and the kitchen sink.

As for mutation, I agree. I think Rust with hihgher-kinded polymorphism would be my ideal language.


There's always a reason of some kind for the additional complexity. The question is whether it's worth it.

> The extra complexity in their modern form is to pack the setter and getter into a single function, and allow in-place modification (without going throgh getter and then back through setter

In place updating of nested fields is pretty much the bare minimum that any respectable record system provides.


A record system doesn't allow to create fields from arbitrary getter-setter pairs. Lenses are an implementation of property system, not a record system. That the heavyweight solution is also used for regular records because Haskell's record syntax is broken is another matter. I still frequently wish I could use lenses in other languages, but they're hard to implement without HKT (I wouldn't be surprised if someone implemented them for Scala though).

EDIT: And to clarify, by in-place updating I meant in-place semantically, not syntatically (the old solution of keeping a record of two functions can do the syntax part too). But I think I probably misremembered how van Laarhoven lenses work and they might not be ultimately different from getter-setter chaining, sorry.


I don't think you ever need mutation in your business logic. I worked at a Scala shop that bought into immutability etc but was relatively green at it. There was always a pure FP way of solving a problem without mutability.

Also I don't see how ST would especially slow you down if you really want to write pure function with mutability under the hood? It's an interface you have to learn, but the benefit is you _prove_ your mutations are locally-scoped.


For performance reasons mutability in Scala is often necessary. It was a financial shop and many things were too slow to just fold over. Closures are slow, and copying is slow. A lot of what we were doing was adding up and multiplying numbers. The difference in performance between, say, a while loop that adds up a variable in 1 million classes and a fold is so stark.

Moreover it’s not just about performance. Right now I’m programming a bittorrent client in Scala with Akka and it would incredibly difficult to do without mutation. And the complexity overhead that Haskell requires for mutation just isn’t worth it in a lot of cases.

I’m pretty good at determining if mutation doesn’t bleed into the outer scope and most of the time don’t want to pay the complexity overhead in actually proving. Sometimes you do want that of course, but not most of the time.


I wish Haskell had better syntax for records, but to be honest it’s very rare that it actually presents a real issue in practical code that I write. Sure, it can be a bit awkward to update nested records, but I struggle to think of more than a few cases where it’s caused me any real grief, and even then it was only an issue of convenience (having to write a slightly more awkward line of code) as opposed to causing bugs or preventing a satisfactory solution to a problem. I readily admit it’s something that could be improved, but I think it’s more of a hobby horse than a real problem. YMMV of course. For what it’s worth, I’ve written tens of thousands of lines of Haskell over the past few years and have barely ever used lenses. They’re really there as a convenience for people who are familiar with them. Understanding lenses is by no means a prerequisite to writing practical code in Haskell.

I find it hard to co-sign on your assertion that all non-trivial programs require mutation. This hasn’t been my experience at all. While having access to side-effects is certainly essential (which Haskell is fully capable of doing), mutation is quite rarely needed in my experience. In the cases that it is, IORefs are trivial to use. On top of that Haskell has MVars, which I hazard to claim are the best concurrent mutation primitive I’ve ever had the pleasure of using. Quite the opposite of being crippled.


I'd like someone to blog about Haskell who has say an average IQ, has learned Haskell (used it professionally) and then can come back and talk to points like this.

It seems a lot of Haskellers have super high IQ, and can grok Haskell Lens like it's a toy truck. Then they write a blog post that's pretty hard for a beginner to disect.

Or maybe they struggled with Lens but by the time they spent 5 years with other gurus in a professional setting they finally got it, but have forgotten how hard it is to learn this stuff.



Absolutely - that's why I added the professional experience caveat. Someone who has just got a concept 5 minutes ago shouldn't write a tutorial about it.


Learning it is challenging but achievable by most interested programmers. It's vastly different than most mainstream languages so it's not as easy to pick up as C# is to a C++ programmer because so few concepts translate from C++ to Haskell. But it's not impossible if you're seriously interested.

You also don't have to learn all of it to start using it. The majority of code I write in Haskell doesn't use anything more fancy than function composition, ADTs, and some type classes. You can do programming at the type level but that's not a requirement for entry.


You might like to take a look at the de facto standard Haskell introductory textbook, http://haskellbook.com/, by Christopher Allen and Julie Moronuki. They are not category theoretical geniuses. Julie is from a linguistics background I think.


Lenses are much more powerful than just records. It's programmable ., too borrow the metaphor.


Yeah, I think it would need to be a language feature. Otherwise the memoization function itself, with state and conditional execution based on the state, isn't very straightforward.


In simple cases (for example a function UInt8 -> Something) it might be reasonable to expect the optimiser to know how to memoise automatically. But how about functions of the form (Integer -> Something), which have an essentially infinite domain? How much space is the memoization allowed to use? What kind of data structure should be used for the lookup? This could be very hard for the application programmer to communicate to the compiler.

Memoising a function f in Haskell can be done like this: create some lazy map where the keys are arguments, and the associated value is f(argument). The function f will only be evaluated once you actually try to use the values of the map.


I thought the first example in this blog post on TH was an interesting idea: https://www.parsonsmatt.org/2015/11/15/template_haskell.html


Best case scenario would be the optimizer does it for you. Less optimal would be compiler hints saying 'please memoize this function'.


Can you really do it in python with an arbitrary function? What if your function takes a dictionary, set, or some other unhashable type as an argument?


lru_cache doesn't support unhashable types.


Nice, but it misses the point that `IO` is actually pure (referentially transparent) in Haskell:

  let x = putStrLn "hello"
  in do x ; x
is equivalent to:

  do putStrLn "hello" ; putStrLn "hello"


I don't think it really 'misses' this point, so much as it elects not to touch on this. I think the point is that the effectfulness of a value is captured in its type, but if you're trying to explain this to someone that isn't familiar with haskell, it would be somewhat more confusing than saying what was said.


I though purity was about having no side-effects. Or, in other terms. If you call the a pure function twice with the same arguments, you get the same output.


You do get the same output. main returns a monadic data-structure which tells the Haskell runtime what to output in response to certain inputs.

See: http://conal.net/blog/posts/the-c-language-is-purely-functio...


Nitpick but technically there is not a Haskell runtime, only compiler (at least if we're talking about ghc)


GHC does have a runtime system, conveniently called RTS. This page has some more details:

https://downloads.haskell.org/~ghc/latest/docs/html/users_gu...


There is most definitely a haskell runtime... what do you think handles garbage collection in haskell programs.

Haskell also needs a runtime to execute its evaluation strategy which is not call stack based, but graph reduction based.


Those two statements aren't actually equivalent.

Haskell isn't pure. A truly pure programming language would be completely useless as it wouldn't be able to actually manipulate state at all.


No! Haskell programs are actually pure!

The main function returns a _description_ of the actions it's going to do; it doesn't actually execute those actions. You can pass these descriptions around as values, extend them, match on them and what not.

Think of it like Command pattern in OOP. When you pass a command around the command doesn't actually execute. It's a value that at some later stage will be executed.


Is there a way to distinguish between a main function that "actually" executes the actions, and one that does not? I think that laziness makes this a subtle question.


The distinction would become rather clear if you call that function from other code, I imagine. In other languages things would happen, while here you would just get the return value to do what you want with.


there are no functions that actually execute actions. That function is in the Haskell RTS, and not in your program.


Isn't that very close to being a vacuous semantic distinction at this point? I don't see much of a difference to "My C# main never actually executes. It's the .NET runtime that interprets the IL bytecode."

I'm still a beginner, but having to figure out when and where to flush text to the console doesn't make haskell feel different from other imperative languages.


> I don't see much of a difference to "My C# main never actually executes. It's the .NET runtime that interprets the IL bytecode."

You can't actually reason about C# programs in those terms - e.g. you can't really say whether two C# programs are equivalent except in the vacuous sense of being equal as strings or ASTs. Some basic C# functions offer only operational semantics, so you have to reason about how and when those functions are "actually executed" if you want to be able to understand programs that call those functions. E.g. you can't know whether "(foo(), foo())" is equivalent to "x = foo(); (x, x)" without thinking about "how foo() is actually executed". In contrast you can reason about Haskell programs that contain IO actions without having to understand how those IO actions are executed.


In this sense even C programs are pure.


There is a famous blog post about this: The C language is purely functional. http://conal.net/blog/posts/the-c-language-is-purely-functio...


The runtime manipulates (external) state, the language remains pure.


I thought it was neat that they talk about trying to parse the "J" programming language.


Yes, direct link[0]. Though most of the subjects are interesting in my opinion.

[0] http://www-cs-students.stanford.edu/~blynn/haskell/jfh.html


Due to the purity, programming in Haskell is like learning how to configure a system, instead of coding.

Configuration is always pure, only the underlying runtime system is not.


the post focusses on the practicality of haskell and addresses purity but leaves the biggest problem out: laziness, which is more or less the reason haskell exists, is the wrong default. In particular lazy I/O which can introduce horrible bugs or straight up mess with execution order of critical code, which in my opinion is an absolute no-go in an industrial language.


Lazy I/O is widely regarded as a mistake, I don't really know any Haskell programmers that advocate that anymore. We have plenty of other approaches that have a better foundation.


Most popular languages have aspects that you can consider failed experiments. Like java's 1.0 libs or C's declaration syntax.


Are you referring to the various streaming libraries (Conduit, Pipes and the like)?


That is what people use instead of lazy IO.

And those libraries aren't just "hacks" to deal with laziness or purity. They're wonderful APIs for writing constant-memory streaming programs. I always wish other languages had libraries on their level.


Lazy I/O is not especially popular. So I wouldn't really say it's a problem with Haskell since most Haskellers would use other solutions.

And Haskell is cutting-edge in more ways than laziness so I don't think this is a big deal. It's _technically_ "why it exists" but in practice there's a variety of other reasons people pick Haskell nowadays besides laziness.


This is something non-Haskellers worry about quite a lot, and Haskellers not much if at all. Others have mentioned that lazy I/O is not really a thing in real world Haskell. But its important to know that lazy by default has a lot of benefit in the composition of pure functions, e.g. `take 10 . sort`


> a lot of benefit in the composition of pure functions, e.g. `take 10 . sort`

At the risk of catching HN's ire... this is not a good thing.

Haskell's type signature tells you almost nothing about whether this use-case is supported. If it's not, you've got yourself a nonlinear slowdown, potentially wrecking your performance. Readers can't know this without literally reading the documentation, except the documentation doesn't even tell you if this is OK.

Yet because people are being ‘clever’ like this, Haskell has ended up stuck with a sort a factor ~ten slower than Python (!!), and an incremental sort that's still a factor ~2 slower than Python's heapq-based incremental sort.

So you're using a sort in a way that you have no explicit language-level guarantees for, seemingly no documented guarantees for at all, and in a way that's incredibly harmful to the common case, but also—and this is a curse that's almost unique to Haskell—the language is also prevented from utilizing future improvements to sorting algorithms!

Because of this, and despite Haskell's ‘purity’, the internals of Haskell's sort are more observable and less open to change than is the case for almost any other language that I know of!


Indeed. It's bizarre that some Haskellers push this as an example of one of the benefits of Haskell when it's implicit behaviour, exactly what I use Haskell to avoid. If, on the other hand, sort were `[a] -> Stream (Of a) ()`, well, that would be something else ...


>this is a curse that's almost unique to Haskell—the language is also prevented from utilizing future improvements to sorting algorithms!

Back in reality of course, Haskell has fast, generic, linear-time sorting: https://hackage.haskell.org/package/discrimination


I've been a proponent of radix sort since long before I'd heard of this package, but I think you've completely missed my point.


Sorting has to operate on the entire input, how is laziness an advantage here?


Sure the entire input list will need to be consumed, but not every element of the output will need to be determined if we are only taking the top 10.

Laziness enables many compositions to work efficiently, whereas with a strict language, you'd have to fuse the operations to get the same efficiency. More examples here: http://augustss.blogspot.com/2011/05/more-points-for-lazy-ev...


If you are running a sort them you will need to compare each of the elements at least once which will mean you have to evaluate the whole list right?


You have to evaluate the whole input list (absent cheating by checking if you have enough elements equal to minBound), but you don't have to sort more elements than you need to produce, which can save you work.


Nope! Image sort is quicksort or heapsort. Then the work will be O(k log n) vs O(n log n)


I can see how this might be possible but does it actually work like this in practice?

Is your k here representing “take k . sort”?


Yes. Let's say sort here is quicksort (a reasonable choice). Quicksort works, as you know, by partitioning based on a pivot element, putting those smaller before the pivot and those larger after the pivot. It then recursively sorts the two partitions. In Haskell this recursive sort is deferred until the result is actually needed, and a smart haskeller implementing the quicksort would choose to do the smaller-than-pivot partition first--as is the default syntactic choice in any case when concatenating two lists. On each recursion the smaller-than-pivot branch is taken and the larger-than-pivot branch is deferred. As soon as condition is hit where the pivot is the smallest element, it is immediately returned to `take`. The `take` implementation then calls for another value if k>1, and it resumes from where it left of, recursing into the previously deferred larger-than-pivot partition at the bottom of the execution tree, and working itself up from there until `take` is satisfied. Once `k` elements are taken, there's still a partial tree of unexecuted continuations that is thrown away.

In other words, the very nature of Haskell's lazy-by-default semantics makes the straight-forward, seemingly strict implementation of quicksort, or other sorting algorithm, automatically optimized to improve performance through lazy execution.


because maybe later in the program you will deicide that that computation does not need to be executed at all.


Can you give an example of what you mean by this?

That’s the kind of thing you would use “if” for right?


One was given earlier in the thread: `take 10 . sort`

This should be read from right to left: it sorts an (implicit) list, then reads the first 10 elements from the beginning of the now-sorted list, and throws away the rest.

Except under the hood in Haskell, sort is not a function that takes a list and returns a list. It is a function that takes a list, and returns the smallest element of that list and a function to fetch the rest of the sorted list. (A function which actually returns the second-smallest element and a function that if called returns the third smallest element and another function, etc.)

These ephemeral functions / continuations / "thunks" are allowed to have internal state and thereby perform more complex sorting operations such as quicksort, and might actually be represented by an internal tree of branch points and their returned value or unexecuted thunks.

But all of this complexity is hidden by the compiler. It's not some special property of `sort` either, which might look like this:

    sort [] = []
    sort (x:xs) = sort small ++ (x : sort large)
        where small = [y | y <- xs, y <= x]
              large = [y | y <- xs, y > x]
"The sort of an empty list is the empty list. The sort of a non-empty list is the elements of the tail less than or equal to the head of the list, sorted, followed by the head of the list, followed by the elements of the tail greater than the head of the lest, sorted."

The Haskell compiler does the magic of turning this into a lazy-optimized implementation which in the case of `take 10 . sort` doesn't bother to calculate any remaining `sort large` partitions once the first 10 smallest elements have been found.


> lazy I/O which can introduce horrible bugs or straight up mess with execution order of critical code

I've never seriously programmed Haskell, so honest question: I understand the first point, but how is the second possible? Isn't the point of passing RealWorld around that you enforce the order of execution through data dependencies rather than expression order? It always seemed like a very elegant (and incredibly impractical :) ) solution to me.


OP is somewhat conflating two different things: non-strict function evaluation and lazy IO. With lazy IO, you can get, for example, a String from a file. That string is actually a lazily-constructed chain of Cons cells, so if you're into following linked lists and processing files one char at a time, then it's fun to use. The dangerous bit comes in when you close the file after evaluating its contents as a string:

    fd <- open "/some/path"
    s <- readContentsLazy fd
    close fd
    pure $ processString s
Now, processString is getting a string with the file's contents, right? Nope, you have a cons cell that probably contains the first character of the file, and maybe even a few more up to the first page that got read from disk, but eventually as you're processing that string, you'll hit a point where your pure string processing actually tries to do IO on the file that isn't open anymore, and your perfect sane and pure string processing code will throw an exception. So, that's gross.

That's a real issue that will hit beginners. There's been a lot of work done to make ergonomic and performant libraries that handle this without issues; I think that right now pipes[0] and conduit[1] are the big ones, but it's a space that people like to play with.

[0] - https://hackage.haskell.org/package/pipes [1] - https://github.com/snoyberg/conduit


Seems like the problem is that file close is strict, whereas the file handle should be a locked resource that is auto-closed when the last reference is destroyed (and “fclose” just releases the habdle’s own lock).

In other words the problem seems to be (in this example) that the standard library mixes lazy and strict semantics. A better library wouldn’t carry that flaw.


So that's actually how it works if you just ignore hClose. The problem is that it sometimes matters when things get closed, so they do "need" to expose the ability to close things sooner.


Sort of. It eventually gets cleaned up by the garbage collector, yes. But that could be after an indeterministic amount of time if the GC is mark-and-sweep. My point is that in this circumstance reference counting could be used regardless so that as soon as the last thunk is read, the file is closed. The 'hClose' is basically making a promise to close the file as soon as it is safe to do so.


> as soon as the last thunk is read, the file is closed.

That's probably doable. It's true that when the only reference to the handle in question is the one buried in the thunk pointed at by the lazy input, it should be safe to close it when a thunk evaluates to end-of-input (or an error, for that matter).

I'm not sure whether or not it'd be applicable enough to be worth doing. The immediate issues I spot are that a lot of input streams aren't consumed all the way to the end, and that you'd have to be careful not to capture a reference anywhere else (or you'll be waiting for GC to remove that reference before the count falls to zero).


Also things like unix pipes or network sockets, where the "close" operation means something different as there are multiple parties involved. Arguably the same is true of files as you could be reading a file being simultaneously written to by others.


Right. It's easy to handle the simple case, but honestly "let the GC close it" works fine in the simplest cases.


Why is it possible to "close" a file? You could have a function that mapped open files to closed files, but the open files would still be there... I think the reason why this weird behavior is cropping up is that the entire language is designed around functions, and here you are reaching into the internal datastructures, mutating state.


> Why is it possible to "close" a file?

Because the program we're compiling needs to work on actual computers, running under actual (usually at least vaguely POSIX) operating systems. In that context, it's unavoidable that the set of open file descriptors sometimes matters. It can matter because of resource limits. It can also change whether another process gets an SIGPIPE versus blocking forever. It can affect locking.


> Why is it possible to "close" a file?

i guess i would ask why it's possible to close a file that's going to be used after it's closed? will linear types[1] solve this?

[1] https://gitlab.haskell.org/ghc/ghc/wikis/linear-types


> i guess i would ask why it's possible to close a file that's going to be used after it's closed?

I don't think there's much reason to want to do it, but it's not obvious how to enforce that while still retaining the flexibility we'd want.

Linear types expand the solution space, to be sure. Whether they "solve this" depends a bit on exactly what we consider the problem to be.


The file isn't going to be used after it's closed. The string from the file is going to be used after the file is closed. But with lazy IO, you don't have (all of) the string from the file yet, even though you've "read" it.

That is, the abstractions don't do what non-Haskell abstractions would lead you to expect.


Right. The whole point of lazy IO is that you hide the actual IO behind values that don't appear to be IO. That means your use of the file isn't visible to the type system, so it's not really reasonable to expect it to prevent it. Unless I miss something, you can't write lazy IO without lying to the type system anyway (unsafePerformIO).


Monadic IO allows you to reliably sequence IO operations, but the results of those operations are not produced until their results are forced. Here's an example of the problems with this: http://dev.stephendiehl.com/hask/#lazy-io . It's not really a language flaw, but more of a design flaw of the functions in base.

Almost nobody uses lazy IO anymore though. Most IO is done w/ strict IO functions from the bytestring library and often with stream processing libraries like conduit or pipes.


> Almost nobody uses lazy IO

... in anything large. If I'm writing something small of the form "read stuff from stdin", lazily consuming the results of getContents is a perfectly fine way of shipping data through the program. As soon as it starts getting complicated, though, it becomes important to refactor (which is often super easy in Haskell).

I actually treat unsafePerformIO pretty similar, though with more of a requirement that the project be expected to stay tiny. Worth noting that it's much more important you understand Haskell's evaluation model in that case. ... assuming what you're doing matters. If you're just having fun, it can be a good way to learn a little more about Haskell's evaluation model.


You're absolutely correct however Haskell has a couple of function that let you circumvent it's normal evaluation rules which you can see in https://hackage.haskell.org/package/base-4.12.0.0/docs/Syste....

These are sometimes useful for optimization but in an unfortunate bit of history, some developers decided to use these to "simulate" Haskell's lazy semantics for side-effecting I/O operations, the so-called "lazy I/O".

Fortunately this is just a few functions in the standard library which you can simply avoid and serious Haskell codebases should use a linter to prevent people from calling them.


relevant quote from the linked docs:

> `unsafeInterleaveIO` allows an IO computation to be deferred lazily. When passed a value of type IO a, the IO will only be performed when the value of the a is demanded. This is used to implement lazy file reading, see `hGetContents`.


Lazy IO refers to a set of functions like readFile that explicitly break that abstraction, tying observable IO to the order expressions are evaluated. Under the hood, the lazy IO functions rely on a GHC function called unsafeInterleaveIO, whose name hints at how it works and that it's not safe.

The fact that lazy IO behaves so unintuitively and causes significant problems strongly signals that that particular abstraction shouldn't be broken.


Lazyness in Haskell can never change I/O (maybe except performance) unless you use unsafePerformIO, right?

Also, there's a link to a page about laziness on the left.


I guess it's explained elsewhere here aswell, but the "lazy IO" discussed is pretty much a bug: you read a file to String, but this String is loaded from the lazily while you consume it. But the problem is: you already get the String thinking all is well, but if something happens to the file handle you get a runtime error. This shouldn't happen with pure values.


Used to code quite a bit in Haskell, but I've moved on. I believe in the philosophy "Simple things should be easy, hard things should be possible"; Haskell forgets the first part.


Languages influence each other. Few people have used OCaml but most will get to use OCaml-influenced features in a mainstream language like Swift, Rust, Kotlin, Scala, etc.


I read the article, but I am confused. Where is the "Fan Site". Is this a tutorial for creating a fan website?


I believe it’s all the links at the bottom represent his fan site.


Here's a chunk of code from a haskell project I work on that shows how concise and nice haskell can be:

    class EntityStore b m => TagStore b m where
        -- | Add a new tag
        addTag :: b -> Tag -> m (Either DBError (ModelWithID Tag))

        -- | Get all tags
        getAllTags :: b -> Maybe Limit -> Maybe Offset -> m (Either DBError (PaginatedList (ModelWithID Tag)))

        -- | Find tags by a given ID
        findTagsByIDs :: b -> [TagID] -> m (Either DBError (PaginatedList (ModelWithID Tag)))
That bit at the start (`EntityStore b m => TagStore b m`) roughly reads "Given that types 'b' and 'm' exist that satisfy the typeclass 'EntityStore', they satisfy the typeclass 'TagStore' if they implement the following methods".

If your eyes glazed reading the sentence above -- this is a way of composing interfaces. Any TagStore-capable thing is required to be EntityStore-capable.

And here's that being used:

    createTag :: ( HasDBBackend m db
                 , HasCacheBackend m c
                 , MonadError ServantErr m
                 , TagStore db m
                 ) => SessionInfo -> Tag -> m (EnvelopedResponse (ModelWithID Tag))

    createTag s t = requireRole Administrator s
                    >> validateEntity t
                    >> getDBBackend
                    >>= \db -> getCacheBackend
                    >>= \cache -> addTag db t
                    >>= ifLeftEnvelopeAndThrow Err.failedToCreateEntity
                    -- invalidate cached tag listing & FTS results
                    >>= \res -> invalidateTagListing cache
                    >> invalidateTagFTSResults cache
                    >> pure (EnvelopedResponse "success" "Successfully created new tag" res)
I prefer the explicit 'bind' syntax (>>/>>= are pronounced 'bind') to do notation, because of the clarity you get when the code is laid out, and how it encourages modular functions

All those `HasDBBackend m db` and `TagStore db m` incantations mean that this function knows about the world is that it has a DB backend, a cache backend, a tag store, and it knows something about the kind of errors it can throw (`ServantErr`). Then come what the actual function does (after the =>) -- Take in a SessionInfo, a Tag, and produce an "action" that when run will produce a EnvelopedResponse (ModelWithID Tag) value.

This style is called mtl and it's just one of the big patterns in the haskell community, but the big feature here is that isolation -- this function can only do things that it knows about by way of constraints (`TagStore db m` means a `TagStore`-capable thing is accessible to you) -- this is much safer than having functions that can just do anything at any time.

I've said it numerous other times, but the kind of stuff you do in haskell trickles into other languages -- see Florian Gilcher's talk from RustLatam 2019[0] -- it's basically on this same concept but in Rust (one of the reasons I absolutely love rust, they've bolted on a fantastic type system).

[0]: https://www.youtube.com/watch?v=jSpio0x7024


Haskell is fun, lots of fun, but it is not a practical language by virtue of it being strongly typed.

By practical I mean strongly typed PLs are not forgiving as your world and perceptual view of the world changes over time -- as it does for any business.

Your program must pass a proof to execute. For the most part you do not need to explicitly express your types; Haskell can infer them. This is often pointed at by Haskell enthusiasts as some kind of nicety. But it's not because (1) the types are still there; your program still has to cohere wrt to the type system; (2) strongly typed programs with explicit type annotations are much easier to read and manipulate, so you should use them anyway.

As your world and assumption changes over time you will find yourself having to do a ton of work to have everything cohere for the type proof again. Haskell enthusiasts also like to say things like with strong typing "Refactoring is easier". This is incorrect. What they are pointing at is that Haskell will of course catch certain (type mismatch) bugs during a refactor, but it will also find many many things that would not be a bug were you using a lighter or no static type system. So ironically this in my experience discourages refactoring because you become exhausted from all the unnecessary labor required to make rather nominal changes to your domain model.

Dynamic languages on the other hand, when in the right hands, don't obligate this whole unnecessary labor of effort.

I feel that there's an elephant in the room whenever I talk to someone who is enthusiastic about strong typing in a business or real world context. And that's this: that their enthusiasm is more to do with the pure fun of manipulating a type check/prover and playing in these weeds than actually getting real stuff done over time.

Another common retort from the strong typing enthusiast is they will point back at a dynamically or loosely typed system they've worked on and point out what a mess it is and then proceed to show how they don't run into certain classes of bugs any more, like null pointer exceptions. What goes unanalyzed is the competence of the team that made the mess, that it was the fault of the skills at play and not a necessary fault of not having types. What also goes unanalyzed is whether the extreme cost of playing the strong typing game is worth an occasional NPE (which are of the immensely easy and fixable kind of bugs) here or there -- in a standard business (ie, not mission critical) context.

I realize this is going to be hotly contested and that I'm stepping on toes here, but I think all of this is true.

Learn Haskell by all means, but be honest with yourself when comparing it to the successes of more pragmatically designed languages. This should also be no surprise; Haskell is a research langauage born in academia, NOT born out of long practitioner experience. Compare to Elm -- another strongly typed language for the frontend -- which was born out of a doctorate with limited real world experience. Then compare to, say, a Clojure (another esoteric language) but which was born from extensive pragmatic experience and look at the choices it made and how in many cases they run against Haskell's.


> with strong typing "Refactoring is easier". This is incorrect.

I can't tell if you're just trolling, but have you ever done a large refactor in a dynamically typed language such as Python? It's a runtime minefield, taking a huge amount of testing effort to gain any level of confidence that you got it correct. In a language like Haskell or Rust, it's usually as simple as getting the thing to compile.


IME a code base with high test coverage lets me refactor far more securely than an expressive static type system. Static typing is useful, but if I had to pick one it would be tests all the way.


The cost of a high test coverage could be an additional 3x lines of code for the tests. While you still need tests with an expressive type system you can basically cut the code coverage to a fraction. And maintain the same level of confidence in refactoring.


While you still need tests with an expressive type system you can basically cut the code coverage to a fraction.

Where on earth do you get the idea that static typing lets you cut the code coverage to a fraction? I am going to need specific examples to believe this claim.


> Where on earth do you get the idea that static typing lets you cut the code coverage to a fraction? I am going to need specific examples to believe this claim.

I don't know how I can give a "specific example" when the example is effectively my whole programming career (most of which was proprietary codebases). But it matches my experience: code with static types and a fraction of as many tests ends up with fewer production bugs than code with substantial test coverage in a dynamic language.

Think about how much of your code contains actual business logic rather than just plumbing, and imagine never having to test the plumbing parts, only the actual logic. For most functions there's only one possible thing for that function to do, the type tells you that it does that thing, so there's no need for a test. E.g. a (parametric) function that takes a set and returns a sorted list can only ever sort that set or return an empty sorted list, so the only thing you need to test is that the returned sorted list is non-empty.

https://spin.atomicobject.com/2014/12/09/typed-language-tdd-... gives some examples (in Java - deliberately using a language where types are very inefficient to show that they're effective even then).


Using a comprehensive and a well crafted type system limits the choice you can input to your functions. Drastically. If you utilize the type system right it essentially forbids you from supplying faulty data as input to your functions.

After a large range of (faulty) inputs are already forbidden by the type system you're left to write tests that actually test the business logic. I don't see how this would not cut the required test coverage compared to for example dynamic type systems where virtually any input is allowed and has to be covered by tests.


This is all false.

You can get input data verifications dynamically or statically. You don't have to use static type verification to verify data inputs and dynamic validations are much more expressive and simpler. See the immensely expressive Clojure spec or Ruby Validations and compare to any type system.


Couldn't find anything on Ruby validations (drop me a link?) just something about the db ORM/engine.

However, Clojure's specs indeed seem to be runtime checks. Seems very different from a type system, really just packages functionalities for the primitives rather than having an expressive type system.

I may be understanding this wrong but I fail to see what's the added benefit of moving checks to runtime in contrast with the performance penalty it produces. Or maybe it's just an improvement to an otherwise dynamic language, like Erlang/Elixir has Dialyzer.

Seems to me it's more of a mechanism to handle the errors or produce code that handles inputs a static type system would prevent from being used in the first place.


Runtime checks are much, much more flexible (because they are ad hoc), more compose-able (they are written using the PL itself), _and_ more capable (you have the full breadth of the PL semantics).

I could go on with concrete examples to demonstrate each of these merits but here is a big one:

In a statically typed architecture you typically see a typed domain object (ADT) used -- eg, Person -- and you let the type itself "validate" (eg, Person.name, Person.age are non-null/required fields, etc). And wherever Person flows you are obligated to adhere to this form/validation requirements OR you must create a new type and map from/to these two types. This is already obviously a bad idea.

In a dynamic language you can define specs universally (like you are obligated to in the static typing case) or you can define a spec for different scenarios and different functions.

Let's say I have a process (sequence of function calls) that ultimately cleanses/normalizes a person's name (I'm making up a use case here). This code does not need age but in a statically typed system you will be passing around a Person object and all sources of data must "fill in" that age slot. Now there are a lot of responses from the statically typed people to this scenario [1] but if you follow them all down honestly, you will end up with a dynamic map to represent your entity and you will best be within a dynamic system like Clojure to optimally code and compose your code.

[1] The only other response is to say who cares if I have to "fill in" age and to this person I say I feel sorry for the people six months, one year, etc that have to build on top of your code.

Here's the Ruby link you asked for-- it's actually rails,b ut it ca be fully decopuled from ActiveRecord fwiw: https://guides.rubyonrails.org/active_record_validations.htm... (Spec is far more decoupled and designed for general use.)


Not an expert on spec, but I suspect that there are a lot of data that are not representable with combinations of predicates: GADTs, rank-n types, existential types, type families, data families, and other more advanced type system features.

Either way, the tradeoff is this: statically typed languages are guaranteed to be free of type errors, but no such guarantees can be made with runtime assertions. Whether or not you use a tool like spec to help you make those assertions, the fact that they happen at runtime prevents them from making any guarantees about type safety. Even if you use spec literally everywhere, as you suggest (and no one actually comes close to doing anyway), you are still not guaranteed of type safety. Dijkstra explains why: https://www.cs.utexas.edu/users/EWD/transcriptions/EWD03xx/E...


I will say that immutability helps a lot. Clojure is dynamic but immutable-oriented, and refactoring with deferentially transparent S-expressions is a delight— + you don’t need type system for this.


This is exactly my point.

If your goal is to optimize for a future “big refactor”, then your problem is a present-day skills problem, not a PL issue.


It seems like you're arguing that dynamically typed languages are best because they don't get in the way of programmers who are good enough to pretty much never screw it up in the first place. Am I misunderstanding you?


More or less, yes.

But I also worry about the team that thinks a statically typed language is going to save them.

I think at the end of the day you have to do this job well or you don’t get any guarantees. I’ve seen people make equal messes in strongly typed languages and dynamic; the PL is not the thing. But one thing that is true is that strongly typed languages tax you strongly; anyone who says otherwise (and enthusiasts often do) is lying even if to themselves as well.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: