1. 33
  1.  

  2. 14

    Haskell is a unique vehicle for bringing modern programming language theory (PLT) to a production-quality programming language

    I am not contesting that Haskell is not a good research vehicle. However, could you name some innovations from Haskell that have been added to other modern languages that were not already a part of ML?

    Just remember that the idea for neural networks is from the 1950s.

    artificial neural networks as a notion, yes. And even though they were an inspiration for perceptrons, a perceptron is just a basic linear classifier (of the form y = sign(ax + b)), with a very crude optimization algorithm (compared to modern algorithms, such as LBFGS). Of course, people did consider other architectures, but arguably modern neural networks only started in 1975 when Werbos proposed backpropagation and his work was not barely noticed until the mid-1980s. So that puts us in a similar timeframe as Miranda, Haskell’s predecessor. The largest problem in neural networks at the time was the lack of computational power and large data sets. Everything to build feed-forward nets, which are still competitive in some tasks, was already known in the mid to late eighties. Even a lot of architectures that have become popular the last half decade, such as LSTMs, bidirectional RNNs, etc. have been around for a long time, but were simply computationally too expensive.

    So comparing Haskell to neural networks is comparing apple to oranges. Neural networks were for a large part bottlenecked by limited computational power and data, which is definitely not true for functional programming languages.

    I agree that Haskell has produced a lot of useful ideas. But it is very unlikely that Haskell will explode in popularity as neural networks did.

    1. 29

      However, could you name some innovations from Haskell that have been added to other modern languages that were not already a part of ML?

      • The obvious one is definitely typeclasses and other derivatives there. Scala and Rust are clearly inspired by Haskell’s typeclass design as well as OCaml’s modular implicits.
      • The second obvious one is laziness. Other languages have not adopted this directly, but instead tend to adopt it piecemeal. Understanding how far one can push laziness in Haskell has influenced other languages and other libraries.
      • GADTs were undoubtedly a feature that Haskell played a huge part in pioneering and have been adopted by Scala at least. One could see this as another step in exploring the use of existential types which began in OCaml, but it’s a new approach. Other languages have chosen not to include GADTs, but even then they are reacting to essentially research performed by the Haskell community.
      • Free data structures and embedded DSLs in that style were both heavily explored in Haskell. These, of course, can be explored in other languages and certainly were in research communities, but Haskell was one of the first language communities to really push the idea.

      Finally, last and most, is the notion of purity as a practical and desirable construct. OCaml took a definite stance on favoring purity by allowing and encouraging mutation in controlled locations. Haskell took that much further and inspired large techniques in pure programming. Monadic designs and interfaces are the obvious innovation here, especially as applied to impure internal languages, but you can also see research on STM, effect systems, regionalized mutation, and more as being pushed much further by Haskell.


      All that doesn’t mean I don’t agree with you that NNs aren’t really a comparable metaphor, but as far as I can see Haskell already has “exploded on to the scene” in pushing both specific research ideas and, perhaps most importantly, popularizing them.

      There isn’t a fraction of the posts on “why you should learn OCaml” as there are on Haskell (though, I think there should be). That’s a silly metric, but it’s a metric that aims at the point Haskell has played in popularizing these ideas as well.

      1. 5

        Just wanted to say thank! I appreciate the extensive reply. I did not really take Scala into account and Scala definitely adopted a lot of Haskell language features.

      2. 4

        C# and VB.NET LINQ query comprehensions were influenced by Haskell.

      3. 9

        I believe these points apply much more to Racket than to Haskell.

        1. 6

          Lispy languages in general. Haskell does have DSL support, though. Thinking on it, I’d say the innovation was happening in both but differently. The Haskeller’s tend to use different techniques than Lisper’s. Especially more mathematical and strongly-typed stuff. Good for innovation to have them going in parallel.

          1. 3

            Full ack. It’s nice to have research and practicality mixed. We need more innovation that’s directly usable.

            There’s some cross-pollination going on too. For example, lazy evaluation is something that SICP already introduced in Scheme before even Haskell existed. Possibly it was taken from Miranda or its predecessors. But that’s my point; we need this kind of innovation and great languages to include this stuff. For example, as far as I know, parser combinations come from the ML family of languages (at least popularized by Wadler and Hutton) that many languages with good closure support have nowadays, too. And we’re better off with this.

            What I don’t like is languages taking every new thing that is invented and integrating it deeply in the language. For example, in JavaScript I still believe classes are a bad fit; it subverts the fundamental idea of prototype inheritance behind JS. Languages should have differentiating features and styles. You see it with PHP as well - it’s going more and more into a direction that makes it look like Java. And in Python, they’ve added async/await and type annotations. Especially the latter feels out of place in the language.

            1. 5

              Minor point, lazy evaluation has been defined and discussed at a research level for many years. I don’t know when the research began, but you could see the beginnings of it in discussions of non-confluence of lambda calculus normalizations. So everyone is stealing from very old ideas here.

              Scheme’s innovation was to make laziness available cheaply as a language-level idea via macros. Haskell’s innovation was to make laziness pervasive and default everywhere.

              Finally, laziness really needs to be considered in the light of abstract data types as well. The theory of data types, of data and codata, is a really cool and important element of the modern theories of evaluation semantics. Again, this wasn’t invented (nor is it even truly a concept in) Haskell, but Haskell is important in being a vehicle for popularizing these ideas.

        2. 5

          Haskell is a unique vehicle for bringing modern programming language theory (PLT) to a production-quality programming language

          What bothers me with this quote is the latter part. What makes Haskell a production-quality language? There is only a single compiler. There are so many different dialects that different team members will write different code hampering later understanding for anyone joining the club. On top of that, debugging and deployment tools are lacking. In addition, upgrading language and libraries gets one in a funny situation that not everything works due to something down the chain not being updated. Finally, support between different platforms is so different in quality that support becomes a nightmare in fighting GHC.

          1. 10

            This is an odd comment. I’ve been an almost exclusive full time Haskell developer for the last 3 years, using the language as the main workhorse on multiple profitable products. I find it funny that you’re bothered by the notion that Haskell is production-quality.

            There are so many different dialects that different team members will write different code hampering later understanding for anyone joining the club.

            My experience has been the exact opposite. My latest project involved many Haskellers many of whom never saw each other. And there have been periods in the project when no Haskellers were left. Which means the lore of the program were carried purely through the code, and yet, this project has seen constant improvement without a single rewrite. I was able to push my first features and bug fixes into production within the first week I’ve joined with virtually zero introduction to the codebase. I understand where your “dialects” prejudice is coming from, but it doesn’t apply to Haskell for some reason. Maybe the extremely strong type system coupled with purity makes it so that it’s always easy to understand what your predecessor meant.

            On top of that, debugging and deployment tools are lacking.

            Haskell has a debugger, though I’ve never bothered to try it, because functional programming (code is split into small independent functions) + a good REPL means you don’t need to step through your code to understand it. Well, even “stepping through code” has the sequential mentality built into it, but I digress. From this point of view, OOP languages aren’t production quality because you don’t have a good REPL experience with them, even when the REPL is good as is the case in Python, because OOP and its twisted notion of encapsulation is hostile to REPLs.

            I don’t know what you mean by a deploymeny tool? The compiler compiles a binary and you deploy it using whatever…

            upgrading language and libraries gets one in a funny situation that not everything works due to something down the chain not being updated

            Never experienced that, upgrading to never dependencies has always been a breeze. You get 2-3 stupid type errors and you fix them quickly and it just works.

            1. 3

              Never experienced that, upgrading to never dependencies has always been a breeze. You get 2-3 stupid type errors and you fix them quickly and it just works.

              I think you must have gotten lucky. For example, these days I am fighting regex-base, which is an old and unmaintained package that lies at the root of the fairly popular regex-* package hierarchy, and which has become broken in GHC 8.8 due to the MonadFail change. There are lots of cases like this for every new GHC release. It usually takes a month or two of frustrating poking to shake it all out. For GHC 8.8 specifically I also remember that Happy (the parser generator) and vector-binary-instances (some other foundational utility package) needed code modifications.

              1. 3

                Why do you want to upgrade to GHC 8.8 before it hits a stackage lts though? Before there’s a stackage lts, I regard new GHC releases to be previews for library authors to catch up.

                My comment applies to the case where you give a new GHC release a decent amount of time to settle before upgrading.

                1. 5

                  How do you think GHC 8.8 becomes ready for Stackage? It’s due to library users going through all their dependencies and fixing all these small things. The Haskell ecosystem has a relatively large amount of important packages with relatively absentee maintainers (sometimes because they are busy with other Haskell things), and it can take quite a while to chase all of them down when new GHC releases come out.

                  Just because you are not the one feeling the pain, that doesn’t mean someone else isn’t paying the cost. While I do use Stackage, I usually have to go through a bunch of busywork to get my dependencies re-added after every GHC release. (I’m actually so exhausted of it, that this time I think I will stick with just getting the fixes on Hackage and then hope someone else deals with Stackage, and maybe just abandon Stackage entirely.)

                  1. 4

                    While this is definitely true, I’m not sure how it impacts “production readiness”. Many languages face serious issue when they make backwards-breaking changes. Some just outlaw these. Most companies which use those languages in production thus work only conservatively to follow these updates, or may not at all.

                    From a business production-readiness POV, the risk is either (a) older versions of GHC will eventually become completely broken due to loss of some critical infra and my strategy of holding back will bite hard or (b) new growth in the community is hampered so completely by some change that I can’t participate in upgrades when I want to pay down the internal cost.

                    For a lot of “production” languages, only (a) matters because people just won’t upgrade after a while. I think Haskell is largely safe for (a) because it maintains old packages (is idemopotent). The work you do is deeply appreciated and enables both (b) and general health of the community under new development (non stagnation).

                    1. 1

                      I’m sorry, I was really focused on the “production-quality” point that I didn’t even consider you were talking from a library-maintainer/author standpoint. I really appreciate all the effort people put into making stackage lts’es into what they are. The dream package manager experience in my opinion. I think the ergonomics of maintaining a Haskell package is also very important, but I don’t think it affects the standing of the language in application development as long as Stackage snapshots are so well.

                2. 2

                  Maybe we have different experiences. I was dealing with codebases written by different teams and each is a separate islands with their idioms and use of pragmas. Onboarding security researchers without prior knowledge of Haskell was a nightmare, because the learning surface just kept expanding in front of their eyes.

                  REPL doesn’t mean it’s production ready. Production ready (at least for me) means having the tools to deploy to a large scale cluster, to inspect the running machine, to be able to introspect the running system for all necessary metrics (the last one would mostly apply to languages running on a VM).

                  1. 2

                    Onboarding security researchers

                    I imagine this could be a problem. Learning Haskell is a significant effort.

                    the learning surface just kept expanding in front of their eyes

                    I wonder why the domain experts had to understand all of Haskell? I don’t know the circumstances, but you often try to expose a DSL to domain experts, or at least make them responsible for a part of the codebase that’s mostly written in an EDSL, so they don’t have to know what monads are.

                    Production ready (at least for me) means having the tools to deploy to a large scale cluster …

                    I think I’m missing something; What’s wrong with just producing a binary, build a docker image around it and deploy it as such and get all the features of its ecosystem.

                3. 7

                  If you use the nix-style install and build commands available with cabal 2 and later, libraries aren’t installed globally, and the early problems with upgrading globally installed libraries go away. I suggest “cabal new-build” and “cabal new-install” for all projects.

                  Similarly, ghcup is a good way to install multiple versions of the GHC compiler installed at the same time, and easily switch among them.

                  1. 8

                    I suggest “cabal new-build” and “cabal new-install” for all projects.

                    These are the default as of Cabal 3 which was released a few weeks ago.

                  2. 4

                    Nothing against Haskell, even though I don’t agree with “unique” part, but the update part is really done much better in the OCaml ecosystem, and that’s one of the things keeping me there. It’s dead simple to keep multiple compilers on the same machine, import/export installed package lists, and test any latest versions and experimental flavours without breaking your main development setup. You can vendor libraries, too.

                    1. 2

                      These days I use ghcup ( https://github.com/haskell/ghcup ) to install and switch among multiple versions of the GHC compiler. It’s very much a copy of rustup. At the moment it supports ten different versions, though I only have two versions installed.

                      What’s the OCaml tool that does the things you describe above?

                      1. 3

                        As @sanxiyn says, it’s opam, the standard package manager.

                        1. 1

                          Presumably parent is referring to opam and opam switch.

                        2. 1

                          OPAM is good, but Stack IMO is better. The compiler installations in Stack are implicit, the choice of the compiler depends on your stack.yaml, and you can have multiple stack.yamls in a project to test with different compilers&package-sets.

                        3. 4

                          No company actually cares about their programming languages having multiple compilers in 2019.

                          1. 4

                            There will only ever be a single compiler for Scala. Nobody will be able to successfully recreate its behaviour.

                            1. 0

                              Monoculture is such a great concept, doing wonders around the world. Just look at bananas.

                              1. 3

                                Monoculture existential risk is real. It’s also tiny. It’s the annoying low prevalence, high priority style risk.

                                So, yes, we’re all fucked if GHC/bananas goes off the deep end and dies somehow and we institute other kinds of management techniques to mitigate that risk. For GHC, we have steering committees and community-led development which both seek to limit the rate at which bad decision-making can kill the project.

                                1. 1

                                  Yes, and it’s a good thing there is a well structured steering committee. But my comment was in jest, as one of the great things of multiple compilers is that they will focus on different aspects, bringing a lot of good to the ecosystem. Or, at least, companies could aim for something that of interest. I constantly look at Java ecosystem, and for better or worse, there are some pretty good runtimes there.

                                2. 1

                                  Because compilers are exactly like horticultural diversity, of course.

                            2. 4

                              I spent 8 hours in Haskell trying to do the equivalent of os.environ[‘FOO’] in python.

                              Probably says more about me than Haskell. But if you ever want to kill 8 hours, try googling “get encironment variable Haskell” and then try putting the equivalent of if (variable) { this } else { that } into an existing program.

                              Maybe I just got unlucky due to the structure of the program, since it was set up where an IO result was somehow unacceptable. But the ultimate trick was to use something like “unsafeEval” which actually evaluates the expression. No matter how hard I googled, I failed to find this trick out in the wild.

                              None of this is to smackttalk Haskell. It’s a different world.

                              1. 11

                                I think this says more about the stereotypical Haskell tutorial material than about you or Haskell.

                                Language X tutorial:

                                1. Hello, world!
                                2. Slurp something from input, lightly munge it, and then print it right back out
                                3. [Usual stuff about standard types and control flow constructs interspersed with fancier examples that often do some useful IO]

                                Stereotypical Haskell tutorial:

                                1. Factorial (optionally preceded by “Hello, world!”)
                                2. Fibonacci numbers (lazy lists) or “sieve of Eratosthenes” (actually trial division) or “quicksort” (actually not in-place)
                                3. [Usual stuff about lists and algebraic datatypes and pattern matching interspersed with fancier examples, often including an expression calculator, but nothing about IO apart from getContents and putStrLn. IO would be, like, somewhere in the second half or maybe even near the end.]

                                Here’s more of the former and less of the latter:

                                At some point you’ll come across the question “what’s a monad?” for the first time, and the correct answer at this point really should be “it doesn’t matter”. You don’t need to know about monads to do IO, you just need to know that IO actions return IO values (IO String or IO Int instead of String or Int), that when you write your own IO actions (like main) you can use a do block, and that using <- inside a do block unwraps the IO value so you can pass a normal (non-IO) String or Int to your normal (non-IO) functions, munge it, and then pass it back to some IO actions to output. At no point do you ever need to use any function with “unsafe” in its name. It might seem like doing extra work for no reason, but that’s how you have slightly increased confidence that you’re not launching missiles while calculating your factorials because the types say so.

                                1. 3

                                  Haskell By Example is amazing and its creator is my hero.

                                2. 9

                                  Does this help any? https://gist.github.com/shapr/7c0fe351f78da33505f640b1b3e12ffe

                                  I usually structure my code by doing all side effects at the top level and passing the values to the pure code for processing. checkEnvVars is pure code, and the side effects are executed in main.

                                  1. 2

                                    Kind of! How would you do the equivalent of turning the PORT environment variable into an integer, falling back to 8080 if it’s unset? It’s honestly eluding me…

                                    1. 8

                                      The function you want is readMaybe. Usually there’s enough information from the context that the typechecker can infer which type you are trying to read into. To then go from your Maybe Int to an Int (by providing a default), you can either use a case-expression and pattern match, or the fromMaybe function:

                                      import Data.Maybe (fromMaybe)
                                      import Text.Read (readMaybe)
                                      import System.Environment (lookupEnv)
                                      
                                      main :: IO ()
                                      -- Explicitly:
                                      main = do
                                        portVar <- lookupEnv "PORT"
                                        let port = case readMaybe portVar of
                                               Nothing -> 8080
                                               Just p -> p
                                        someFunc port
                                      
                                      -- With fromMaybe:
                                      main = do
                                        portVar <- lookupEnv "PORT"
                                        someFunc (fromMaybe 8080 (readMaybe portVar))
                                      
                                      someFunc :: Int -> IO ()
                                      someFunc = undefined
                                      
                                      1. 3

                                        Thank you (and everyone else) for the helpful replies!

                                      2. 5

                                        I’d go with something like this:

                                        import Control.Monad ((=<<))
                                        import System.Environment (lookupEnv)
                                        import Text.Read (readMaybe)
                                        import Data.Maybe (fromMaybe)
                                        
                                        main :: IO ()
                                        main = do
                                          host <- fromMaybe "::"   <$> lookupEnv "HOST"
                                          port <- readDefault 8080 <$> lookupEnv "PORT"
                                          putStrLn (host ++ " " ++ show port)
                                        
                                        readDefault :: (Read a) => a -> Maybe String -> a
                                        readDefault dfl = fromMaybe dfl <$> (readMaybe =<<)
                                        

                                        There are three things to grok.

                                        1. <$> is an fmap alias. It is part of the Functor type class.

                                          • In the context of Maybe it means “apply function on the left to the thing inside of Just; skip when Nothing”.

                                          • In the context of IO it means “apply function on the left to whatever value the I/O operation on the right eventually produces”.

                                        2. =<< means (in the context of Maybe) “do nothing if the thing on right side is Nothing, otherwise pass whatever is inside the Just to the function on the left and use it’s result”. This is part of the Monad type class.

                                        3. lookupEnv returns IO (Maybe String). The monads are nested here.

                                        1. 1

                                          Unfortunately I was in a situation where I was using a third party library (hakyll, a common blog platform) and was not able to pass an IO String into the setExtension function. I didn’t have control over their function and therefore no opportunity to restructure it to use IO. It was expecting a plain String.

                                          I guess unsafePerformIO might be the right answer there, but someone else here says it’s a disaster waiting to happen.

                                          Here’s the line of code: https://github.com/shawwn/wiki/blob/7dfe54f4ed6b5f6ec021915fdeed057be71187ea/Main.hs#L69

                                          1. 2

                                            setExtension accepts a String because it doesn’t want to trigger arbitrary side-effects while trying to work out what extension to set, which is what it would do if it accepted an IO String and bound the result via either do-notation or (>>=) (which is what do-notation desugars into).

                                    2. 7

                                      the ultimate trick was to use something like “unsafeEval” which actually evaluates the expression. No matter how hard I googled, I failed to find this trick out in the wild

                                      Unless you’re a very advanced user doing some very low-level and unsafe things that might depend on compiler version, unsafePerformIO and related functions are less an “ultimate trick” and more a disaster waiting to happen.

                                      As other commenters are noting, the thing to do is to restructure your code to do the IO somewhere else and pass in the value you need.

                                      1. 1

                                        It was a third party library hakyll that required a String rather than an IO String. I wonder if there’s any other solution in that case… https://github.com/shawwn/wiki/blob/7dfe54f4ed6b5f6ec021915fdeed057be71187ea/Main.hs#L69

                                        1. 6

                                          There are at least two other options, depending on your needs and haskell fluency:

                                          1. Read the envvar before you call hakyll, while you’re in IO instead of Rules. You can then either:
                                          • Pass the value into the functions that need it directly, or
                                          • Use a ReaderT String Rules monad to make the envvar available (if you’re comfortable with transformers, and/or have a lot of places where it’s needed).
                                          1. Use preprocess :: IO a -> Rules a to lift the envvar lookup into Rules, and then run it just before it’s needed.

                                          EDIT: I would probably lean towards #2 in this instance, unless I had multiple places where the envvar was needed. Then I’d refactor towards argument passing or transformers.

                                          1. 5

                                            Yes, that’s what you want: https://github.com/shawwn/wiki/pull/1

                                            I did this with no knowledge of Hakyll. Only knowledge of Haskell. Took 2 minutes. Types and purity are awesome.

                                      2. 5

                                        Others have given concrete advice, here’s the conceptual advice:

                                        Gathering data from the ENV is a side-effect. From within your program, you cannot be sure when these values change or how they are controlled. We often treat them as though they’re fixed, unchanging global data, but good Haskell practice has us minimize assumptions of this form.

                                        So, the standard trick for Haskell application development is to have a “startup” phase where resources are acquired and configurations are read all into static, internalized locations which are then fed (purely) to the remainder of the app.

                                        Concretely, my advise is to make a “Configuration” type and store in it all of the values which you require from the environment. Then, build values of this in IO during “startup” and pass instantiated values of this to the rest of your program during operation.

                                        This feels bad at first because it smears the linkage between behavior of your program and the state of the ENV variables across two locations. It ends up being way better though as you’ve now factored the logic of your application away from the logic of gathering configuration information. This is excellent for testing, fuzzing, or repackaging the app into new environments.

                                        1. 1

                                          You’re completely right, but I don’t think it makes a lot of difference for a beginner in which part of the program reading the ENV variable doesn’t work ;) Just saying, it’s not really easy if doing it as you described, it’s just maybe even harder doing it at runtime, deep in another function.

                                          1. 1

                                            I agree that it busts the “I can do whatever I need to, whenever I need to, to solve my problem” mentality, but that, itself, is an assumed thing. Purity changes the rules of the game in a way that’s quite pervasive.

                                            The “startup phase” trick leads to better design. That’s one consequence of the reduced effect induced by purity.

                                            I don’t want to try to argue whether one is worse or better for beginners. I know that working with purity is harder for programmers coming from other languages though.

                                        2. 2

                                          Nope, it’s not you. I had the same experience with Haskell twice and then again with PureScript.. Doing some IO-heavy stuff in a completely new language usually takes me a few hours. In Haskell and PureScript I needed a few days until it worked. See infinitesimal’s comment, with which I fully agree. Most Haskell tutorials seem to be written backwards, also mostly by zealots. I’ve been told I “don’t get” functional programming several times when I criticized stuff.

                                          1. 2

                                            What does this have to do with OP?