The thesis of laziness by default isn't "everything should be lazy", just that there's some benefit to having strictness be something you add as necessary - maybe that it's easier to add strictness to something lazy than add laziness to something strict, that it gives you more abstraction power, or maybe increases surface area for optimizations.
Similarly, the existence of generators and zero-argument functions in Python doesn't undermine the idea that strictness by default could have benefits.
Not just games - toys like Hatchimals and LOL dolls are just loot boxes targeting an even younger demographic, with similar unboxing videos to market them.
Something to keep in mind - linear types are on their way[1], with exactly this usecase in mind. Simon Peyton Jones gave an excellent presentation on the topic[2], briefly discussing exceptions, as well as giving a mention to ResourceT and the phantom type solution in the article (described as channel-passing).
I'm not convinced the current linear types proposals actually let us solve the problem, in the presence of exceptions. I may very well be missing something, or it may be that exceptions are rare enough that leaking resources until garbage collection only when an exception occurs is fine in practice.
FWIW Rust doesn't consider leaking an object to be unsafe. In fact, there's a std::mem::forget() function that can be used to do precisely this. Before Rust 1.0 it was marked `unsafe`, but it was determined that the existence of Rc allowed for creating retain cycles in safe Rust, and "fixing" this was prohibitively complicated, and since it was possible to leak values in safe rust, std::mem::forget() was therefore determined to not be unsafe. This was also when "leaking values isn't a violation of safety" was turned into an actual principle, as opposed to just a hunch that a bunch of people had, and the one API that relied on leaking being unsafe was removed (this was the API for scoped threads, which was marked as unstable right before 1.0 came out, and subsequently an alternative stable safe API for doing scoped threads was hashed out in the crates ecosystem).
Haskell has such an extensive set of language extensions, I would say adding new features to the type system is probably the MOST Haskell-ish way of doing things.
The explicit purpose of Haskell is to be a basis for research into functional language design (edit: among other purposes). By "explicit purpose" I mean exactly that... people got together in 1987 to come up with a language for research. Haskell was never supposed to ossify into some kind of "finished product", it was built exactly so people could experiment with things like linear types. If you want to just write libraries and get stuff done with a more or less fixed language, you probably want to be writing OCaml.
I do normal, boring line-of-business programming in Haskell every day.
I think Haskell does have a good model for bringing together practical application of theoretical research.
Parent's comment is spreading the myth that Haskell is an academic language. It's not wrong but it's not Haskell's only stated purpose or utility by far.
I used to do normal, line-of-business programming and I stand by the comment.
If it sounds like I'm saying that Haskell is not useful for boring, line-of-business programs then I wasn't clear... Haskell is a research language, yes, and not exclusively so. But I'm confused why it's objectionable to spread a "myth" if that myth is, in your words, "not wrong". The stated purpose of Haskell, when it was created, is a matter of historical record.
> It should be suitable for teaching, research, and applications, including building large systems.
This, to me, means that we are not going to freeze the language, and sacrifice research, in order to support business applications. That would go against the goals of the language.
Doing everything as a library seems "un-Haskellish" to me because there's an ongoing and vibrant community that's doing research into things like type theory, which can't be done as libraries, and kicking that group of people off the Haskell platform just to support business applications would be a failure of Haskell as a language.
The myth that gets circulated by critics of Haskell is that it is an academic language and has no practical use in industry.
I think your post was unclear and supported that myth. After reading your reply I understand better what you meant!
I agree -- extensions do seem to be working rather well. I hope the new Haskell standard, Haskell2020, will include some of them into the language proper!
I'm looking forward to seeing how linear types work/interact with the rest of the language.
For what its worth, when I read the parent comment, I did not at all get the impression that it was "spreading the myth that Haskell is an academic language."
They are fixing omissions related to full dependent types, many of them.
Compare language features and Haskell's approach: Erlang and distributed-process, goroutines and channels and Control.Concurrent(.Chan), (D)STM is a library, Control.Applicative and Control.Monad for many things hardly expressible in any other language, etc, etc.
Linear types, I am afraid, would go the way implicit parameters went - their use is cumbersome and they really do not help much with day-to-day programming and when they are needed they can be perfectly implemented with a library.
From a language design perspective it makes a lot of sense to add linear types to the language itself instead of using an encoding. Every encoding that I know of (such as region types encoded as monads, which is what I think the article wants to get at) leads to excessive sequentialization of code. This in turn leads to a lot of boilerplate (or crazy type inference problems) at compile time as well as suboptimal run time performance.
Linear types are the perfect example of a feature that belongs in the core language, or at the very least into a core intermediate language. They are expressive, in that you can encode a lot of high-level design ideas into linear types. You can compile a number of complicated front-end language features into a linearly typed intermediate language. Linear types have clear semantics and can even be used for improved code generation. If we ignore the messy question of how best to expose linear types in a high-level language then this is just an all around win-win situation...
Have you took a look at Clean the programming language? It has unique types (used for resource management, but less restrictive than linear types) for decades and guess what? They invented special syntax (the "#-notation") which introduce shadowable names much like regular monad syntax does. And code with this syntax is, basically, sequential code most of the time albeit unique types allow for something like "where" clause. You just easily get lost with these handle, handle1, handle2... names.
I do not oppose inferring linear use at core and/or intermediate representation (GRIN allowed for that and more). I just do not see their utility at the high level, in the language that is visible to the user.
It's extremely difficult to do this and maintain even the figment of usability.
Unless, of course, you're implying it's very haskellish to implement libraries with huge usability gotchas (of which ResourceT was one until the Ghosts of Departed Proofs paper reminded us we can reuse the machinery of ST), then I totally agree.
There was an abstract of PhD thesis devoted to enhancing usability of DSeLs by helping with error messages - they had to be expressed in terms of DSeL, not Haskell-the-language. And linear types as a library (be it ResourceT or something other) is a DSeL.
I think it is a better venue which can help many applications simultaneusly. While linear types won't.
I am always impressed by what the ocaml/Haskell people can do compared to my language of choice (scheme).
Iirc Oleg Kiselyov implemented proper delimited continuations in ocaml as a library, without touching the runtime or compiler. Something similar has been done in Haskell.
I doubt fully dependent types can be implemented in Haskell without extra help by ghc. There has been lots of work in the area, and last time I checked you could simulate DT to some degree, but it never was as powerful as the dependant types in idris. Iirc t were some edge cases where the typing became undecidable.
>Iirc Oleg Kiselyov implemented proper delimited continuations in ocaml as a library, without touching the runtime or compiler.
To clarify this, the library you're talking about implements most of the functionality in C, reusing the runtime's exception mechanism. So it doesn't require any upstream change to compiler or runtime, but it also can't be implemented in pure OCaml.
It's possible if you have dependent types and are not afraid to (ab)use the type system. See section 2.4 of my thesis (link in bio) for a taste. You have to squint a bit but a system like that can ensure linearity.
Linear types amounts to modification of environment - "use" of linear value removes it from environment, so you can't eat cake and still have it. If you look at the use of unique types in Clean, you will see that their use closely reminds monadic code (e.g. "#" syntax). Otherwise you will need to invent variable names like handle1, handle2, handle3... to refer of "modified" handles generated after each use.
E.g., hClose will have type like (Has listIn h, listOut ~ Del h listIn) => h -> ParamMonad listIn listOut () and hGetLine will result in type much like this one: (Has list h) => h -> ParamMonad list list String
It is not perfect: you still may have reference to handle after it's use and you may be tempted to introduce it somehow back and get run-time error; you also would struggle juggling two handles for copying files, for example (for this you may have to use ST parametrization trick).
But anyway, you really not need linear types all that often (they are cumbersome - I tried to develop language with linear types, type checking rules and types get unwiledy very soon) and when you do, you can have a library. Just like STM, parsers, web content generation/consumption, etc. Linear types do not warrant language change.
This concept is a big part of what the "big deal" around monads is - using monads to model effectful code conveniently puts the information of "this should be shell code" into the type, in a way that ensures that code that calls it also gets annotated as "shell" code. Monads are of course also a much more broadly applicable abstraction, but their application to effectful code, enforcing this design, is usually the first and most apparent place people run into them in the ML family of languages.
I disagree, although it's possible I only disagree with how you've phrased it.
Monadic interfaces in the context of non-deterministic effects are a consolation prize. They represent a way to combine effectful code, but ideally your code would have almost no effects at all.
As far as I can tell, the idealized version of this talk is a batch interface: one effect to grab all the data you need, transform the data, and then one effect to "flush" the transformed data (where flush could mean to persist the data in a database, send it out as commands to control a robot, etc).
Tracking side effects in your types (maybe what you were going for?) is helpful for measuring to what degree your code fails to adhere to this idealized model. If most of your code has an effect type, that's probably a sign to refactor. It also keeps you honest as to the infectious nature of effectful code by propagating the type as necessary.
I don't think we disagree in spirit - I didn't mean to imply that it prevents you from, e.g., writing all of your code in the IO monad, just the points you made in your last paragraph. So, more that they're a useful tool to help you realize these goals, not something that gets you there on its own. It does let you broaden/specify your definition of "effectful" a bit - modelling event streams with monads gives you FRP (as in your robot example), and I vaguely remember reading in some paper somewhere the suggestion of using monads to separate out unconstrained recursion/non-totality/co-data from total code.
The big problem with monads is that they are still imperative calculations even if individual effects are nicely typed. If functional code uses them, it effectively becomes imperative. To keep benefits of functional style one want mostly avoid monads. The whole idea of the article is to use any notion of imperative patterns only at the very top to glue things together.
Look at any do block in Haskel, PureScript, Idris etc. It is the imperative code. The individual effects are typed and separated, but it is still the code that depends on implicit state with all its drawbacks.
Then look at Elm code. Elm does not have any imperative hatches. The monad that runs everything is at the very top level (“shell” as the article calls it) and hidden.
As such Elm code is forced to use functional decomposition resulting in very easy to follow, refactor and maintain designs.
Its still quite different than classic imperative code
If you're working with a free monad, or if you don't specify IO (just some of the generic IO like typeclasses like say MonadError), you can still choose your own interpreter for the monad and "program" the semicolon. Which means you get back all the benefits of testability etc.
To get a similar effect in an imperative language, you would use e.g. coroutines and `yield` every side effect to the execution engine. The engine will take the action "specs" (probably a data structure describing the action to perform, e.g. set some value in memory) and decide what to do with them, and you can swap the real engine with a test/mock engine in your tests.
Programming semicolon is not different from mocking interfaces with imperative code. One still has to write it and tests still do not test the real interfaces. Surely the situation is improved compared with imperative code, but it is not as good as with monad-free code.
It is pity that modern conveniences like polimorphic record types with nice syntax for record updates were not invented earlier. With those even with complex code monads can be used only at the top level when the sugar of do blocks is not even necessarily.
Do-notation is Haskell is purely syntactic sugar over function calls. You can remove do-notation from Haskell and still write the exact same programs (with monads and all).
Also, monads are not about state anymore than classes in Java are about Toasters.
Surely a do-block is just sugar for the functional code. But that code can be used to model all imperative effects. As such the code inevitably models all troubles the imperative code can cause.
If one looks at the desugared version one can see where the trouble comes. Functional code using monadic style depends on the state of the monad interpreter that can be arbitrary complex and spread over many closures with many interdependencies. It can be rather hard to uncover what exactly is going on, precisely in the same way as with imperative code it models.
Monads is taking it too far. Mutation is a reality, the correct approach is disciplined mutation. Shoving mutation into convenient boxes and convincing yourself to never look inside it does not mean mutation does not exist. The best approach is taken by scheme, and more specifically clojure to have a disciplined and practical approach. Mathematical purity of programs is a myth propagated by Type theorists dont buy into it.
> Monads is taking it too far. Mutation is a reality, the correct approach is disciplined mutation. Shoving mutation into convenient boxes and convincing yourself to never look inside it does not mean mutation does not exist.
Monads exist exactly because mutation is a reality. Monads do not defy the "mutation reality", nor try to encourage programmers to never look inside them. They are a means of dealing with the "mutation reality" by encouraging to separate pure and impure parts properly and while still making functional composition possible. The image you create for monads is a straw man. Monads ARE a kind of "disciplined mutation" as you put it.
You don't have to like them nor prefer them. But they are clearly a great and established abstraction loved and used by many. You may prefer Clojure, I get it, but I see no reason to talk shit about monads in this way. Have you ever used monads and similar abstractions extensively?
> Monads is taking it too far.
> Mathematical purity of programs is a myth propagated by Type theorists dont buy into it.
Those are big words. Are you some kind of authority? You could have at least prepended "I think" to those phrases.
You are repeating what i wrote, by writing this large comment you havent increased anyone knowledge neither mine nor yours. Monads exist because because haskell people want to pretend that there is this ideal mathematical world where things dont change, some go as far as saying Strong types removes the need for writing tests.
> Those are big words. Are you some kind of authority?
Yrs of writing programs have taught me that programming functions are not equivalent to mathematical functions, there is no equivalence that exist stop pretending that it does.
Once again, monads are a form of "disciplined mutation." They didn't repeat what you wrote, they contradicted your entire premise.
You didn't respond to anything they said and you doubled down with your nonsense about "Monads exist because because haskell people want to pretend that there is this ideal mathematical world where things dont change."
That monads ignore the "mutation reality" isn't a very strong point when monads are a concession for the "mutation reality." Unless you want to repeat yourself a third time, the ball is in your court to bring concrete supporting arguments since you're making the extreme and somewhat self-aggrandizing claim that these other people don't really see the mutation reality of the world like you do, thus they are using inferior tools.
I'd say that anyone specifically trying to corral/isolate their I/O code (monads or not) are so "enlightened" about the mutation reality of the world that they use specific abstractions to address it.
If you want to see code that tries to paper over I/O, look at a program where you can't even tell when and where the I/O is performed because it just looks like any other function call. Active Record in Ruby on Rails might be a good candidate in its effort to make DB access opaque to the programmer.
OK This has become a big feud, apologies for choosing the incorrect words and being rude. Let me put it another way by "disciplined mutation" all i meant was localised mutation. A lexically scoped local variable is enough to handle the spread of mutation, i dont see the need for a specific datatype to handle mutations exclusively. Monads make mutations explicit and global. I hope that makes sense.
I think you misunderstood Monads, or the role type theory has to play in modern programming.
Large projects inevitably benefit from static guarantees enforced automatically by your environment. That can be a 3rd party static code analysis tool or the compiler. Even just a linter will improve code quality and thus developer happiness and productivity.[] Having your compiler enforce* the functional core/imperative shell, and exposing your business logic only through functional components is what makes a strongly typed language of the ML family stand out over, say Clojure.
Mutating state is no problem in a strongly typed functional language. In Haskell, just put your computation in an ST Monad. You can even still expose a functional signature that doesn't leak the ST monad if your algorithm is faster with mutation.
[*] Overall. Some people will probably be unhappier, because they have to follow "arbitrary" rules now, but those would usually have been the worst offenders.
Mutating state is no problem in a strongly typed functional language. In Haskell, just put your computation in an ST Monad. You can even still expose a functional signature that doesn't leak the ST monad if your algorithm is faster with mutation.
That works reasonably well in some situations, but not all.
We often work with local, temporary state, meaning something mutable that is only referenced within one function and only needs to be maintained through a single execution/evaluation of that function. (Naturally this extends to any children of that function, if the parent passes the state down.)
If that function happens to be at a high level in our design, this can feel like global state, but fundamentally it’s still local and temporary. I/O with external resources like database connections and files typically works the same way.
We can also have this with functions at a lower level in the design. An example would be using some local mutable storage for efficiency within a particular algorithm.
However, not all useful state is local and temporary in this sense. We can also have state that is only needed locally in some low-level function but must persist across calls to that function. A common example is caching the results of relatively expensive computations on custom data types that recur throughout a program. A related scenario is logging or other instrumentation, where the state may be shared by several functions but still only needed at low levels in our design.
Now we have a contradiction, because the persistence implies a longer lifetime for that state, which in turn naturally raises questions of initialisation and clean-up. We can always deal with this by elevating the state to some common ancestor function at a higher level, but now we have to pass the state down, which means it infects not just the ancestor but every intermediate function as well. While theoretically sound in a purely functional world, in practice this is a very ugly solution that undermines modularity and composability, increases connectedness and reduces cohesion. And weren’t those exactly the kinds of benefits we hoped to achieve from a functional style of programming?
If anyone would like to read more about this, we had an interesting discussion about these issues and how people are working around them in practice over on /r/haskell a couple of years ago:
There have been many articles on this topic. There isnt any evidence to suggest that static guarantees makes your code better. Ofcourse what does make your code better is immutability. But complete immutability isnt practical and even Haskell people understand that but they continue to pretend that programs are about mathematical purity. If that isnt enough claiming static typing removes the need for testing is complete bunk.
Have you written programs in Haskell/F#/Ocaml? Static guarantees, especially of expressive type systems, absolutely make your code better and their benefits compound as your system gets bigger. The type checker acts as a guardian of the soundness of your whole domain. And yes, expressive static typing removes the need for a whole class of tests, namely the ones that you'd have to write in other languages if you are disciplined enough to care about the soundness of your domain model. I personally loath writing these type of tests but I do when I can't use the power of say Haskell because I care.
Immutability also makes you code better but it's an orthogonal concern and utilising both is a smart move.
Any language that has null, including C++, does not fall into the category of languages with expressive type systems. As soon as you have proper sum types the null issue goes away + the whole big world of working with ADTs open up.
What makes you sure that mutation is a reality? You can just as accurately model reality using an immutable value with time being an additional dimension as using a mutable value without time being taken into account. Both are just models of reality, not reality.
I agree in principle, but this is exactly why we've moved our data pipeline from Python to Haskell.
The Python ecosystem has this concept of what is "pythonic", which, while it results in more code around the internet looking familiar, it largely resolves to "write it out yourself by hand". The result is that there are often no library functions for these things, even in libraries where you'd expect them to be. Need to coalesce an Optional, or need a switch statement? Write out the conditional. Need zipWith, flatMap, or mapMaybe? Write out the list comprehension. Need a combinator, or something curried? Write it out.
We spent an obnoxious amount of time fighting with bugs from this, or weird edge cases in pandas and numpy, that would show up halfway through training, or not at all until we measured results. Heisenbugs from generators were also a huge issue. We ultimately decided the situation wasn't tenable for the quick iteration cycles we needed, and moved all the data munging to Haskell. Almost everything we need for data munging is in base, usually in prelude. If it isn't, it's in lens. Changes and refactors are quick, and no more surprises halfway through training.
I like using zip/map/reduce in my data munge phases as much as anybody and I think python can become a ball of mud too fast sometimes.
But your statement is just so out there to me:
> it largely resolves to "write it out yourself by hand"
me: Oh, I always felt the ecosystem was thorough between numpy, scipy, and bindings to most any database or C code like zookeeper, openCV, redis, etc. but I guess you needed something in a more specialized domain?
> Need to coalesce an Optional, or need a switch statement? Write out the conditional. Need zipWith, flatMap, or mapMaybe? Write out the list comprehension. Need a combinator, or something curried? Write it out.
...
I tried to be charitable, and maybe coming from a language with all of these things these are glaring omissions, but I had trouble no rolling my eyes.
Whatever itertools doesn't have can be done in 2-3 lines. A little repetitive? sure. But so onerous it factors into language decisions? bizarre.
It's not about it being hard to write them out; everything that needs to get written out is surface area for bugs. One missed edge case could just mean that a node of your input layer gets zeroed for some of your rows where it shouldn't, and all you see is no correlation where you expect some. The little bugs that creep in writing this simple boilerplate kill days of work.
One person writes unzip as zip(*arr), another writes it as two separate assignments with the list comprehension written out. It's a tiny bit of code; they both look like perfectly fine unzipping code, and they pass tests, but if you pass one of those code blocks a generator instead of a list - say, someone swaps a list comprehension for map - half your data disappears. No errors, just a node that shows no correlation.
Since the results are all getting serialized anyway, the amount of work to just have that part of the pipeline in a language with some guard rails against that kind of thing is pretty minimal.
I agree that this is very annoying (and time consuming!) time for debugging, as it has implicit assumption about data (e.g. that some column has more than one value).
I was tempted a few time to write pipelines in Python which make such sanity-checks.
In any case - thanks for sharing your example. By any chance, can you show an example for such Haskell pipeline? (Especially if there is some non-trivial statistics, or ML training.)
I'm not familiar with public green spaces in Europe, but there are definitely traditional parks all over Japan. In one place I lived, there was a sizeable park with a nice walking path through the woods just off the train into the city; it was always nice to look out the window and and see all the families hanging out there on the way into town. Where I studied abroad, there was an open park that regularly had town events, but more notably had beer vending machines, so it was always full of loud foreign college students; there was a quieter one with hills and trees where locals walked their dogs a little further away. Osaka castle park also has a lot of grassy area to just hang out; everyone goes to picnic (read: drink) there when the cherry blossoms are blooming. Parks did seem a little more sparse in Tokyo.
To the extent this comment can be construed as anything but content-less partisan dog-whistling, it's pretty off the mark here. Obnoxious as this particular measure is, it was passed as part of the funding for a, relatively speaking, bipartisan infrastructure bill focusing on highways and rail. It's hard to imagine what "real Americans", whoever that's meant to imply, would rather it be spent on.
I'm guilty of being obnoxious? It sounds like you could push that slippery slope until "obnoxious" is simply "any opinion I don't agree with." My comment quite effectively brings up the issue of majority of Americans not wanting to pay taxes for most of the things that taxes are actually going to - we are not being represented effectively. You're the one making this a partisan issue. Just look at some of the polls - or just how many Americans are using other country's infrastructures to avoid excessive US taxes. On topic with this article is the fact that US is collecting taxes worldwide on earnings not even being made in the US - by people who are merely US citizens and not residents! If my comment were so contentless, then you wouldn't have the authoritarian reply that I'm merely whistling for my underclass of "dogs."
I think that's kind of his point - Optionals/tagged unions are "Null values", but only in the places where they semantically make sense.
The problem with nulls isn't strictly null references, in the sense of "a pointer that points to nothing" or "a pointer that throws a null pointer exception", it's that languages with nulls implicitly make null an element of every type. They tend to do this because variables are allowed to have null references, but if you changed the semantics of variables to technically forbid null references, but still had a null value as an implicit element of every type, you'd still have the problem of needing to do runtime checks for null everywhere.
It's intentionally to get across the idea that you don't, and shouldn't, know anything about these arguments within this scope beyond "this is a function" and "this is an argument". These functions just define how to combine things; if you knew any more about the arguments, it would be breaking the abstraction.
The only better names you could really give are ones like func1, func2, arg1, arg2, which don't add any information, just add noise. Trying to get any more specific than that would leave the reader trying to interpret the meaning of the name, but the whole point is that it's strictly "apply this thing to that in this way, without looking at what they are". Even fix-point, which is unarguably cryptic, really needs a paragraph explanation for the unfamiliar more than it would benefit from better names.
That makes sense for the arguments to the outermost function but not to the function it self. Why call it A instead of apply and the then have a big cheat sheet where you put that A means apply and T means applyTo ?
I think those single-letter names like "S combinator" are a holdover from Math notation, where it was a practical consideration, but in practice in functional programming, those names aren't used (I certainly haven't memorized them); If you look at the Haskell column on the page, you can see that they're actually named for readability (though it might not initially seem like it if you don't do a lot of coding in Haskell):
- K is "const" and C is "flip"; they're generally used to for arguments to a higher order function: "map (const 5) myList", "foldl (flip f) 0 myList"
- Psi is "on", since it's regularly used to construct a new function "h = f `on` g"
- S is a specific type of application, called infix as "f `ap` x", but also has an operator to intentionally make it look more like just line noise: "f < * > x". (Had to add spaces to this operator so HN wouldn't interpret it as italics)
The operators might seem opaque, but the idea is to make it more visually apparent that it's a pattern, not some application-specific business logic. There are lots of concepts of "apply" - there's pure application ($), Applicative application (< * >), Monadic application (=<<), etc. - writing them out would distracting to read. Using operators makes it easier to skim and get the general idea of how the code works without worrying about the underlying structural details, while still being precise about them.
- https://softwareengineering.stackexchange.com/questions/2623...
- https://www.reddit.com/r/haskell/comments/6nxgkc/similaritie...
- https://bartoszmilewski.com/category/homotopy-type-theory/