Haskell Fan Site
www-cs-students.stanford.edu
www-cs-students.stanford.edu
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.
[2] https://www.lumi.dev/blog/purescript-and-haskell-at-lumi
I hope Lumi was able to gain efficiencies using PureScript and Haskell. And that they're able to attract talent. Thanks for sharing.
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.
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.
> 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.
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.
https://stackoverflow.com/questions/40130014/why-is-haskell-...
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.
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...
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).
Mercury.co/jobs if you are interested.
costarastrology.com/jobs
Or an extremely long winded "yeah I have the same experience as you."
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.
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.
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.
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.
Why? Is it because of the "fine grained control" part? Because it breaks the neat abstraction?
Painting with acrylics vs Lego.
A point I just realize the value.
Laziness is nice, but not by default perhaps.
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.
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.
> 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
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...
Haskell Programming From First Principle (The most popular in community)
https://github.com/allenleein/brains/blob/master/Zen-of-Func...
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.
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.
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.
"This year was a good year: I made $100,000 at my job although I did lose $500 in the stock market."
But no, it's pretty complicated: https://wiki.haskell.org/Memoization you even have to involve fixed point combinators. Pretty disappointing.
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.
Just like in Python...?
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.
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.
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.
> 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.
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.
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.
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.
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.
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.
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.
let x = putStrLn "hello"
in do x ; x
is equivalent to: do putStrLn "hello" ; putStrLn "hello"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.
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.
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.
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.
See: http://conal.net/blog/posts/the-c-language-is-purely-functio...
Haskell also needs a runtime to execute its evaluation strategy which is not call stack based, but graph reduction based.
https://downloads.haskell.org/~ghc/latest/docs/html/users_gu...
[0] http://www-cs-students.stanford.edu/~blynn/haskell/jfh.html
Configuration is always pure, only the underlying runtime system is not.
Also, there's a link to a page about laziness on the left.
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.
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.
... 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.
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
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.
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?
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.
That is, the abstractions don't do what non-Haskell abstractions would lead you to expect.
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.
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).
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.
> `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`.
The fact that lazy IO behaves so unintuitively and causes significant problems strongly signals that that particular abstraction shouldn't be broken.
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.
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.
That’s the kind of thing you would use “if” for right?
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.
Is your k here representing “take k . sort”?
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.
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...
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!
Back in reality of course, Haskell has fast, generic, linear-time sorting: https://hackage.haskell.org/package/discrimination
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 functionsAll 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).
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.
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.
If your goal is to optimize for a future “big refactor”, then your problem is a present-day skills problem, not a PL issue.
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.
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).
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.
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.
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.
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.)
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...