1. 25

    I did a PhD in maths where I had to do a lot of algebraic geometry, so I’m comfortable with category theory and its concepts and applications. I’ve never seen those ideas being used in nontrivial or useful ways in programming, and by now think that either me or a lot of other people are missing some point. I’m not sure which.

    Category theory became popular in mathematics, and especially algebraic geometry, because it provided a “one higher level” from which to look upon the fields and see that a lot of the ideas we were working with were actually a shadow of a single more abstract idea. For example, the direct products of groups, rings, fields, vector spaces and so on were understood as different incarnations of the category-theoretic product. This helped to standardize a lot of arguments, and give names to some concepts differing groups had been grappling with in isolation before. Grothendieck was able to wield these abstract notions so deftly that he could use them to “take compass bearings” and figure out in what directions he should go. That is unbelievably powerful.

    In programming, I can see how one would model the state of a program as a monad. A monad is basically defined around the idea that we can’t always undo whatever we just did, so that makes sense. I’ve also read a fair number of Haskell programmers and their explanations of how category theory fits into programming. None of it seems to even have the promise of the same levels of usefulness as we’ve seen in mathematics, and a lot of it seems to be actively harmful by raising jargon barriers around trivial ideas.

    1. 8

      That is a great story, I would definitely read more of your writing about math if you shared it somewhere.

      1. 4

        I too have encountered category theory during my maths degree (never managed to get the PhD, though), and I also agree that category theory in programming seems very out of place. The most interesting application I’ve seen for it is in homological algebra, but I’m pretty sure no programmer has any interest in abelian categories. The most prototypical functor for me is the Galois functor, which programmers have no need for.

        The result is that when I see computer people talk about category theory, it’s all utterly foreign to me. They tell me I should like it because I like mathematics, but I do not. I’ve made some effort to understand why they like it and have never been very convinced by it, as unconvinced as you seem yourself.

        1. 4

          I’ve also read a fair number of Haskell programmers and their explanations of how category theory fits into programming.

          I’d be interested in your take on this discussion, in particular the first comment by Gershom Bazerman, as well as his post here. It seems like he has a good perspective on it, but I don’t have the mathematical knowledge to really confirm that one way or the other. Or maybe you’ve already read this particular stuff and dismissed it; in either case it’d be handy to get a sense of what you think to place it in context, if you’re willing.

          Here is another post which your comment reminded me of, which I wish I had the mathematical ability to fully understand; I’d also love to hear what you think about that as well.

          I’m really not trying to challenge anything you said about the misapplication of CT in Haskell/programming in general (if I haven’t emphasized this enough at this point, I don’t think I’m qualified to do so), I’m just always interested in adding more data to my collection, hoping that at some point I’ll have built up the mathematical maturity to understand the different positions better.

          None of it seems to even have the promise of the same levels of usefulness as we’ve seen in mathematics, and a lot of it seems to be actively harmful by raising jargon barriers around trivial ideas.

          As a programmer who barely understands category theory, all I can say is that I’ve personally found the small number of concepts I’ve encountered useful, and, most importantly, more useful than anything else out there (which I’ll generalize as “design patterns and other vague, poorly specified stuff”) for providing a basis for designing modular structures to base programs on. I find that the most basic concepts presented in category theory map well to the kind of abstraction present in programming, and I’d love to get a better sense of where you find the jargon barriers to be and how we could eliminate those (and fwiw I think this is a general problem in programming, not limited to Haskell nerds dropping category theory terms into their discussions). In particular I’ve found concepts like Monoid, Monad, and Functor to be useful–especially in understanding how they interrelate and can be used together. They’ve enhanced my ability to think conceptually and logically about the kinds of structures I deal with in programming all the time, even where I may not be applying these structures directly in whatever program I’m considering. I may be doing it wrong, but insofar as I’ve developed the correct intuition around these things, they seem useful to me.

          So I can readily accept that we have not been able (and maybe never will be able!) to harness category theory at the level Grothendieck did, but it seems like right now it’s yielding results, and part of the value is simply in the exploration and application of a different rigor to programming as a practice. Maybe in ten or twenty years we’ll look back at the folly of applying category theory to programming, but I rather think it’s more likely that we’ll see it as a step on the path toward discovering something deeper, more rigorous and powerful, and more beautiful than what we can imagine for designing programs right now.

          Or maybe we’ll go back to being obsessed with design patterns and UML. If that’s the case I hope I’ll have quit and gone into being an organic farmer in Vermont or something.

          1. 1

            I’m interested in hearing more about this as well. It’s been a long-standing question for me whether continuing to investigate category theory would help me write better programs. I have no background in higher math, but my understanding/assumption has been that category theory is relevant to programming insofar as it facilitates composition.

            As I see it, the fundamental problem of software design is economically accommodating change. We try to facilitate this by selectively introducing boundaries into our systems in the hopes that the resulting structures can be understood, modified, and rearranged as atomic units. I don’t think it’s controversial to say that our overall success at this is mixed at best.

            The promise of category theory seems to be that, rather than relying on hearsay (design patterns) or our own limited experience, we can inform our choices of where to introduce boundaries from a more fundamental, abstract space where the compositional properties of various structures are rigorously understood.

            But like I said, this is very much an open question for me. I would love to be convinced that, although there is clearly some overlap, the fields of category theory and software design are generally independent and irrelevant to each other.

          1. 6

            Yep, this is how I figured out monads too, but when using Rust! There is more to them though - the laws are important, but it’s sometimes easier to learn them by examples first!

            1. 3

              Can you show an example where a monad is useful in a Rust program?

              (I’m not a functional programmer, and have never knowingly used a monad)

              1. 10

                I learned about monads via Maybe in Haskell; the equivalent in Rust is called Option.

                Option<T> is a type that can hold something or nothing:

                enum Option<T> {
                    None,
                    Some(T),
                }
                

                Rust doesn’t have null; you use option instead.

                Options are a particular instance of the more general Monad concept. Monads have two important operations; Haskell calls them “return” and “bind”. Rust isn’t able to express Monads as a general abstraction, and so doesn’t have a particular name. For Option<T>, return is the Some constructor, that is,

                let x = Option::Some("hello");
                

                return takes some type T, in this case, a string slice, and creates an Option<T>. So here, x has the type Option<&str>.

                bind takes two arguments: something of the monad type, and a function. This function takes something of a type, and returns an instance of the monad type. That’s… not well worded. Let’s look at the code. For Option<T>, bind is called and_then. Here’s how you use it:

                let x = Option::Some("Hello");
                let y = x.and_then(|arg| Some(format!("{}!!!", arg)));
                
                println!("{:?}", y);
                

                this will print Some("Hello!!!"). The trick is this: the function it takes as an argument only gets called if the Option is Some; if it’s None, nothing happens. This lets you compose things together, and reduces boilerplate when doing so. Let’s look at how and_then is defined:

                fn and_then<U, F>(self, f: F) -> Option<U> 
                where F: FnOnce(T) -> Option<U>
                {
                    match self {
                        Some(x) => f(x),
                        None => None,
                    }
                }
                

                So, and_then takes an instance of Option and a function, f. It then matches on the instance, and if it’s Some, calls f passing in the information inside the option. If it’s None, then it’s just propagated.

                How is this actually useful? Well, these little patterns form building blocks you can use to easily compose code. With just one and_then call, it’s not that much shorter than the match, but with multiple, it’s much more clear what’s going on. But beyond that, other types are also monads, and therefore have bind and return! Rust’s Result<T, E> type, similar to Haskell’s Either, also has and_then and Ok. So once you learn the and_then pattern, you can apply it across a wide array of types.

                Make sense?

                1. 3

                  Make sense?

                  It absolutely does! I’ve used and_then extensively in my own Rust code, but never known that I was using a monad. Thanks for the explanation Steve.

                  But there’s one gap in my understanding now. Languages like Haskell need monads to express things with side-effects like IO (right?). What’s unique about a monad that allows the expression of side effects in these languages?

                  1. 7

                    No problem!

                    This is also why Rust “can’t express monads”, we can have instances of individual monads, but can’t express the higher concept of monads themselves. For that, we’d need a way to talk about “the type of a type”, which is another phrasing for “higher minded types”.

                    So, originally, Haskell didn’t have monads, and IO was done another way. So it’s not required. But, I am about to board a flight, so my answer will have to wait a bit. Maybe someone else will chime in too.

                    1. 2

                      higher minded types

                      (Just so others don’t get confused, I think you meant “kinded” here, right?)

                      1. 1

                        Heh, yes. Thanks.

                    2. 3

                      A monad has the ability to express sequence, which is useful for imperative programming. It’s not unique, e.g. you can write many imperative programs using just monoid, functor, applicative or many other tools.

                      The useful function you get out of realising that IO forms a Monad is:

                      (>>=) :: IO a -> (a -> IO b) -> IO b
                      

                      An example of using this function:

                      getLine >>= putStrLn
                      
                      1. 4

                        I should say Monad is unique in being able to express that line of code, but there’s many imperative programs which don’t need Monad. For example, just Semigroup can be used for things like this:

                        putStrLn "Hello" <> putStrLn "World"
                        

                        Or we could read some stuff in with Applicative:

                        data Person = Person { firstName :: String, lastName :: String }
                        liftA2 Person getLine getLine
                        

                        So Monad isn’t about side-effects or imperative programming, it’s just that imperative programming has a useful Monad, among other things.

                        1. 2

                          You are way ahead of me here and I’m probably starting to look silly, but isn’t expressing sequence in imperative languages trivial?

                          For example (Python):

                          x = f.readline()
                          print(x)
                          

                          x must be evaluated first because it is an argument of the second line. So sequence falls out of the hat.

                          Perhaps in a language like Haskell where you have laziness, you can never be sure if you have guarantees of sequence, and that’s why a monad is more useful in that context? Even then, surely data dependencies somewhat impose an ordering to evaluation?

                          For me, the utility of Steve’s and_then example wasn’t only about sequence, it was also about being able to (concisely) stop early if a None arose in the chain. That’s certainly useful.

                          1. 2

                            but isn’t expressing sequence in imperative languages trivial?

                            Yes.

                            In Haskell it is too:

                            (>>=) :: IO a -> (a -> IO b) -> IO b
                            

                            But we generalise that function signature to Monad:

                            (>>=) :: Monad m => m a -> (a -> m b) -> m b
                            

                            We don’t have a built in idea of sequence. We just have functions like these. A generalisation which comes out is Monad. It just gives code reuse.

                            1. 1

                              Maybe is an instance of a monad, and there are many different kinds of monads. If you think of Maybe as “a monad that uses and_then for sequencing”, then “vanilla” sequencing can be seen as “a monad that uses id for sequencing” (and Promises in JavaScript can be seen as “a monad that uses Promise#flatMap for sequencing”).

                              Yes, expressing sequence in eager imperative languages is trivial because you can write statements one after the other. Now imagine a language where you have no statements, and instead everything is expressions. In this expression-only language, you can still express sequence by using data dependencies (you hit this nail right on the head). What would that look like? Probably something like this (in pseudo-JavaScript):

                              function (next2) {
                                (function (next) {
                                  next(f.readline())
                                })(function (readline_result) {
                                  next2(print(readline_result))
                                })
                              }
                              

                              with additional plumbing so that each following step has access to the variables bound in all steps before it (e.g. by passing a dictionary of in-scope variables). A monad captures the spirit of this, so instead of doing all the plumbing yourself, you choose a specific implementation of >>= that does your plumbing for you. The “vanilla” monad’s (this is not a real thing, I’m just making up this name to mean “plain old imperative sequences”) implementation of >>= just does argument plumbing for you, whereas the Maybe monad’s implementation of >>= also checks whether things are None, and the Promise monad’s implementation of >>= also calls Promise#then and flattens any nested promises for you.

                              What’s useful here is the idea that there is this set of data structures (i.e. monads) that capture different meanings of “sequencing”, and that they all have a similar interface (e.g. they have all an implementation of >>= and return with the same signature) so you can write functions that are generic over all of them.

                              Does that make sense?

                          2. 2

                            There is a comment below saying it pretty succintly:

                            A monad is basically defined around the idea that we can’t always undo whatever we just did (…)

                            To make that concrete, readStuffFromDisk |> IO.andThen (\stuff -> printStuff stuff) - in the function after andThen, the “stuff” is made available to you - the function runs after the side effect happened. You can say it needed specific API and the concept of monads satisfies that API.

                            Modelling IO with monads allows you to run functions a -> IO b (take a pure value and do an effectful function on it). Compare that to functions like a -> b (Functor). These wouldn’t cut it - let’s say you’d read a String from the disk - you could then only convert it to another string but not do an additional effect.

                            EDIT: I might not have got the wording entirely right. I ommited a part of the type annotation that says the a comes an effect already. With Functor you could not reuse values that came from an effect; with Monad you can.

                    1. 3

                      There simply isn’t a place where, if you put types, it makes your code harder to change.

                      I’m not sure what the author is trying to say here, because it seems obviously and trivially false to me. If I have a function that used to take a String, but now takes a struct/record/sum type with the String as one of its members, the change requires more work if you have to change a number of type declarations in addition to the code.

                      1. 7

                        I think the author, when he’s discussing strong typing, is actually making the assumption that the language in question has full type inference. He acknowledges, in a single throwaway line, that strong typing isn’t enough to make this model work (saying “look at C++”).

                        Some of the static in discussion of types comes from the fact that, plainly stated, only programming language nerds have ever used a statically typed language with sane type inferences. Most people’s idea of static typing comes from exposure to dysfunctional explicit static types in C++ or Java.

                        Type inference is, in my view, absolutely necessary to make static typing less trouble than it’s worth in small and medium-sized projects (and even as static typing sans inference makes certain types of errors easier to catch and more difficult to make in large projects with tens or hundreds of active developers, it requires type inference to make static typing feel like an aid rather than a hinderance even in that situation). Most professional and amateur programmers have never seen it, though.

                        Even as industry is slowly catching up to the past 30+ years of programming language theory, it’ll take another ten or fifteen years before J. Random Hacker hears “static typing” and can reasonably be expected to think of Haskell rather than Java, as the author of the piece does. Until then, if we aren’t explicit about meaning “static typing with inference”, we can expect to get a lot of strange-seeming pushback.

                        1. 2

                          Isn’t the common advice in e.g. Haskell to explicitly declare the types for each function, even if not strictly necessary? Even excellent type inference doesn’t prevent your from having to make changes to all relevant declarations then.

                          Though I now realize that ‘making code harder to change’ and ‘making code more work to change’ are not entirely the same thing. Having to do extra work imposes a small (mental) barrier, but it’s menial work. And as @ddellacosta suggests in the parallel comment, types may make finding the places to change easier, reducing the amount of searching (or following failing tests) to do. So it may even be less work. So I think I agree with the original statement that adding types doesn’t make your code harder to change.

                          1. 2

                            I’m not sure what the common advice is in Haskell. The common advice in Scala seems to be that types should only be annotated in situations where it would make behavior (as opposed to type) ambiguous, and that attitude makes sense to me – it makes using static types precisely as low-effort as using duck types, but moves checking out of runtime to make debugging type mismatches easier.

                            We underestimate the effort involved in menial work to our own peril: programming, in a professional context, is >80% menial by both time and energy (since genuinely intellectually-challenging code is risky – hard to reason about, hard to document, hard for coworkers to learn how to understand, and hard for end users to get an intuition for). Eliminating sources of menial effort makes room for a greater amount of menial effort from other sources.

                            In this sense, I figure having the debugging benefits of strict typing without ever needing to fight the type checker increases productivity substantially: every unnecessary trivial piece of reasoning about types going on in a developer’s head could be replaced with a trivial piece of reasoning about business logic, optimization, UX, architecture, giving less money to Amazon, which movie to attend after work, or something else that’s ultimately more important.

                        2. 5

                          This requires a perspective shift, I think. I found it to be a straightforward and obviously true statement, in that having type information does nothing other than illuminate where a change needs to be made in an expression, vs. having that information obscured. This is the case whether we’re talking about a situation where type inference makes an annotation unnecessary as well as where we may have a type annotation written out: the type checker is going to tell us what we need to change.

                          This is in contrast to a non-statically-typed language where the only way you can understand how types may have changed throughout a codebase is by hopefully exposing enough potential type errors in your tests (or if you have something like Clojure’s spec, through assiduous manual annotation and good practices) so that they don’t pop up at runtime. In this sense “there simply isn’t a place where, if you put types, it makes your code harder to change.”

                          1. 1

                            Yes and see my response to the parallel comment: I was confusing ‘make it more work to change code’ with ‘making code harder to change’. Even if the former would be true, the latter could be false and your argument even makes the former unlikely.

                            Though of course my excellent test suite also illuminates where changes need to be made /s

                        1. 5

                          Rich Hickey: When a library breaks, it can break in many ways. Some of those may or may not be manifest in types, others would just be manifest in behavior, or missing information, or additional requirements - things that you can’t express in types, because most of what your program needs to do can’t be expressed in the type systems we have today. So yes, it still takes a string and still returns a map of information, but it stopped returning you some of that information, or it started returning other stuff, or it had additional requirements about the string… No, the types don’t capture that.

                          It seems like every talk or interview coming from Rich ever since Cognitect started hawking clojure.spec contains at least a handful of poorly supported, vague, dogmatic claims about static typing in opposition to…well, “how Clojure does it.”

                          Perhaps his style hasn’t bothered me up until recently simply because I’ve mostly agreed with his dogmatic statements about stuff like persistent data structures, but at this point I’ve lost interest in a lot of what he has to say because of how he talks about static typing.

                          There are a lot of other ways he could be talking about clojure.spec and why it works well in Clojure. A more nuanced appraisal of the trade-offs of clojure.spec vs. various static typing approaches would be a nice start, but I am skeptical that will happen any time soon.

                          I still think Clojure is a better language than many, and gets a lot of things right, but it’s not perfect and this is one area I think the creator is mistaken. While that’s fine and to be expected, I think he’s doing real damage by making statements about static typing that are either too vague to be useful, or misrepresent how static-typing advocates think about and use type systems.

                          1. 5

                            It seems like every talk or interview coming from Rich ever since Cognitect started hawking clojure.spec contains at least a handful of poorly supported, vague, dogmatic claims about static typing in opposition to…well, “how Clojure does it.”

                            I’m not too familiar with clojure.spec, but I know a bit about the overall idea of it. Let me see if I can explain the difference between static types vs clojure.spec from a formal methods perspective (which differs a little from the PLT perspective).

                            Clojure.spec is a contract system. A contract is a formally specifiable property of the code, usually as pre/postconditions on functions, that you expect the code to follow. You’re able to, through the contracts alone, specify the complete behavior of the function via specs. For example, using Deadpixi Contracts in Python:

                            @require("l must not be empty", lambda args: len(args.l) > 0)
                            @ensure("result is head of list", lambda a, result: result + tail(a.l) == a.l)
                            def head(l: List[T]) -> T:
                                return l[0]
                            

                            With this we have that head is only specified for nonempty lists, and also that its result will always be, in fact, the head of the list. This is not something we can do with the type system of Python, or even the type system of Haskell, as the type of head is indistinguishable from the type of last. We’re calling all sorts of other functions and can, in fact, run arbitrary code. In fact, we can completely decouple the specification of contracts from the verification of them. This is both a strength and a weakness. The strength is that we get both expressive power and flexibility. The weakness is that expressive power is usually a bad thing. In the general case contracts are unverifiable due to the halting problem.

                            In practice, there’s four approaches to verifying contracts:

                            1. Restrict yourself to a subset where you have simple, automatic static verification. For example, you might not be able to autoverify “this function will be called only with positive even numbers”, but we can autoverify “this function will be called with only integers”. This gives us static typing! I think you could reasonably argue that “type systems are special cases of contract systems”, but that’d get you stabbed to death in 99% of programming forums out there, so uh yeah
                            2. Limit verification to throwing runtime errors. Every time a contract comes up, check if it’s correct, and throw an error if isn’t. Most contract-oriented languages combine this with static typing to get “conventional” contract systems. You can do a lot of cool stuff with this. Eiffel’s AutoTest can turn your runtime contracts into integration tests, Ada can place contracts on global mutations, most systems let synthesize contracts into dynamic types, etc.
                            3. Formally prove the contracts correct. This is formal verification. A lot of people are doing this in different ways: Dafny uses pre/postconditions and loop invariants, Liquid Haskell uses refinement types, Idris uses dependent types, etc.
                            4. Informally prove the contracts correct. This is how we get Cleanroom, which is actually a lot more effective than you’d think. People write the contracts, attach english “proofs” of why they hold, and everybody verifies them through code review.

                            So, in summary: contracts generalize static types in a form that is good in some ways, bad in others. There are multiple different styles of contracts, just as there’s multiple different type system, but the unifying idea is that they can fully specify the program’s behavior. In verifying them is another matter, and clojure.spec’s approach is “runtime checks” as opposed to most other languages, which reduce the specification power in return representing contracts as static types.

                            1. 1

                              It’s worth noting that the halting problem also affects Turing complete type systems, such as one found in Scala.

                              1. 1

                                Which is a reason you really don’t want your type system to be Turing complete!

                            2. 3

                              It seems like every talk or interview coming from Rich ever since Cognitect started hawking clojure.spec contains at least a handful of poorly supported, vague, dogmatic claims about static typing in opposition to…well, “how Clojure does it.”

                              I haven’t been paying all that much attention to the talks more recently, but this is also how I feel about it. There are trade-offs between static types and specs, and specs have some advantages over types, but because of all the straw-man arguments it’s hard to find useful analyses of the trade-offs.

                              1. 3

                                The problem is that most of the “types vs ‘specs’” arguments are between people who have languages with types and no ‘specs’ and people who have languages with ‘specs’ and no types. If you want to see a more nuanced comparison, you have to look at languages that have both of them, because then you can see how people proficient in both context-switch between them.

                                The other problem is that there are many more languages with a lot of thought to their type system than languages with a lot of thought to their contract system. If you need both, you’re pretty much limited to Ada.

                                1. 1
                                  1. 6
                              2. 3

                                I fail to see what’s vague or dogmatic about his statements. He’s basically saying that types primarily focus on checking internal self consistency, while what you really care about is semantic correctness. Expressing semantic correctness using types ranges from being difficult to impossible depending on the type system. At the same time static typing can introduce a lot of complexity that’s incidental to the problem being solved. You often end up writing code in a way that facilitates static verification as opposed to human readability.

                              1. 11

                                That is truly amazing work.

                                1. 6

                                  I couldn’t agree more. The amount of dedication and determination this must have taken is quite impressive.

                                  EDIT: Also worth reading about is Jeri Ellsworth, mentioned in the piece as an inspiration.

                                  1. 2

                                    As far as I’ve seen Jeri has all but disappeared from the Internet, I used to follow her YouTube channel quite a bit. It’s a shame, she was a great teacher.

                                    Edit: seems like she’s still active on Twitter

                                    1. 3

                                      I got excited when she started posting about radio stuff about six months ago, but it looks like it was only a short lived return. She was really one of my favourite technical YouTubers back in the day.

                                1. 1

                                  I’ve been collecting resources for automating verification and/or synthesis of programs. Although plenty exist for data, there’s a lot less for control flow. They usually need some clean or at least precise way to specify it that a mechanical translation handles from there. I’m surprised I haven’t seen recursion schemes before this post since it superficially looks like it would help there. Maybe it could be built on CakeML or something.

                                  1. 3

                                    Have you read Functional Programming with Bananas, Lenses, Envelopes and Barbed Wire? I guess you might be interested in a more lang-neutral formalism of this same concept.

                                    1. 1

                                      Thanks for the link. :)

                                      1. 5

                                        This may also be helpful while going through that paper: http://blog.ezyang.com/2010/05/bananas-lenses-envelopes-and-barbed-wire-a-translation-guide/

                                        (Although, it brings it back into the Haskell domain, but still may be more comprehensible than some of the notation in that paper…)

                                  1. 4

                                    I think the elephant in the room that this article doesn’t directly address is “Do the programs you write have many types of data which should never be substituted?”. If all the types in your program really are strings, and you’re just moving text around you would likely be hindered by types. If by contrast you have many different kinds of integer that should not be mixed like weight and account balance you might really benefit from stricter types.

                                    1. 4

                                      If all the types in your program really are strings, and you’re just moving text around you would likely be hindered by types.

                                      I’ve yet to work on a useful program where the data’s representation as a “string” was the highest level of meaning that could be assigned to that data. As soon as you get past writing a toy you will start to have to differentiate between what different string values mean, and then we are talking about types–and arguably at precisely the point where we can start to leverage the power of a (good) type system. Speaking as someone who writes Clojure as his day job, it’s clear to me that this is why core.typed exists, and why there is so much activity around clojure.spec (and schema before it)–it’s obvious that “everything is a string” (or in the case of Clojure, “everything is a map”) isn’t good enough to represent the kind of data we work with as professional programmers (and the jury is out on whether or not clojure.spec is up to the task).

                                      So while that’s not to suggest that we always need a sophisticated type system–or that we aren’t sometimes hindered when forced to use a type system like Java’s–I think it’s fair to say that it is rarely, if ever, the case that we don’t have more meaning assigned to the values in our program above and beyond than their encoding as low-level storage representations.

                                      1. 1

                                        I think another good example of this situation is algorithmic code - the structure one would like to express (e.g. sortedness, sub-solution optimality, graph acyclicity) is often beyond what the type system can represent. You end up with numeric arrays / matrices / edge lists without much further type structure.

                                        At the opposite end is either unit-heavy code (as in your example) or structure-heavy code (e.g. configs / ASTs / requests), both of which significantly benefit from what a typical type system offers.

                                        1. 1

                                          If all the types in your program really are strings, and you’re just moving text around you would likely be hindered by types.

                                          Could you give an example of such a program? I deal with strings relatively often, but the bulk of the logic in my programs (client-side JavaScript) benefit quite a bit from types (via TypeScript).

                                        1. 19

                                          This piece is dumb, and I should probably have just flagged it, hid it, and moved on, but I can’t bear the idea that people would read this and think they’ve learned something about Haskell.

                                          First of all, there are well-known deficiencies when it comes to records in Haskell–there’s been enough written about it, the author could certainly have done the tiniest bit of Googling before writing this.

                                          Secondly, even considering the issues with records in Haskell, you can just write

                                          import Lens.Micro.Platform
                                          -- or import Control.Lens if you're not into the whole brevity thing
                                          
                                          data Person = { _personName :: String }
                                          data Pet = { _petName :: String }
                                          makeFields ''Person
                                          makeFields ''Pet
                                          

                                          …and all those Has-instances are generated for you. You’re done and can do stuff like view name (Pet "Spot") or view name (Person "Barbara") or (Pet "Spot") ^. name if you’re a fan of lens operators…etc. And this doesn’t even scratch the surface of what is possible in Haskell with records, and let’s not forget the amazing shit that is also possible like https://lobste.rs/s/kuuqh5/higher_kinded_data.

                                          Holy fuck! Who the hell does this?

                                          Nobody! Do more research before writing posts like this, please.

                                          1. 11

                                            Apologies to the author, but I find the comparisons to Haskell in this piece to be facile, and the practical examples of ways to solve the “nil problem” in Clojure to be glossing over the fact that nil remains a frustrating problem when nil is essentially a member of every single type (EDIT: although I should acknowledge that yes, the author does acknowledge this later in the piece–I just don’t find the rest compelling regardless). I don’t buy “nil-punning” just because it’s idiomatic (see the linked piece by Eric Normand); as far as I’m concerned talking about it as idiomatic is just putting lipstick on the pig. And fnil is a hack, the existence of which illuminates how Clojure didn’t get this right.

                                            That said, I’m not a lisp guy in my heart, so maybe I’m missing the subtle elegance of nil-punning somehow–but I’d love to see a Clojure without nil in it, and I think it’s telling that these articles always end up defensively comparing Clojure to Haskell.

                                            I believe that Clojure has some nice ad-hoc facilities for building composable abstractions, it’s got a lot of good stuff going for it and makes me want to shoot myself far less than any programming language I’ve used professionally to date. But as a user of the language I’ve found its type system, such as it is, to be incoherent, and I find the pervasiveness of nils to simply be a shitty part of the language that you have to suck up and deal with. Let’s not pretend that its way of handling nil is in any way comparable to Haskell (or other ML-family languages) other than being deficient.

                                            P.S. Per your first if-let example: when-let is a more concise way to express if-let where the else term is always going to evaluate to nil.

                                            1. 11

                                              Glad I’m not the only Clojurian who had this knee jerk reaction. nil is not Maybe t. Maybe t I can fmap over, and pattern match out of. With f -> Maybe t at least I know that it’s a function which returns Maybe t because the type tells me so explicitly and the compiler barks at me when I get it wrong. This gives certainty and structure.

                                              nil is the lurking uncertainty in the dark. The violation of the “predicates return booleans” contract. The creeping terror of the JVM. The unavoidable result of making everything an Object reference. I never know where nil could come from, or whether nil is the result of a masked type or key error somewhere or just missing data or… a thousand other cases. Forgot that vectors can be called as functions and passed it a keyword because your macro was wrong? Have a nil….

                                              Even nil punning doesn’t actually make that sense because it mashes a while bunch of datastructures into a single ambiguous bottom/empty element. Is it an empty map? set? list? finger tree? Who knows, it’s nil!

                                              Edit: wait wait but spec will save us! Well no… not unless you’re honest about the possibility space of your code. Which spec gives you no tools to infer or analyze.

                                              Have a nil.

                                              </rant>

                                              1. 4

                                                That said, I’m not a lisp guy in my heart, so maybe I’m missing the subtle elegance of nil-punning somehow

                                                Minor nitpick–please don’t lump all lisps in with nil-punners! Racket and Scheme have managed to avoid that particular unpleasantness, as has LFE.

                                                1. 3

                                                  Not to mention Shen with a type system that subsumes Haskell’s…

                                                  1. 3

                                                    Even in Common Lisp, nil isn’t an inhabitant of every type. If you add (optional) type declarations saying that a function takes or returns an integer, array, function, etc., it isn’t valid to pass/return nil in those places. I think this is more of a Java-ism than a Lisp-ism in Clojure’s case.

                                                    1. 1

                                                      Isn’t nil indistinguishable from the empty list though, in Common Lisp?

                                                    2. 2

                                                      Yeah, just take it as further evidence that I’m not “a lisp guy” in that I conflated all lisps together here–sorry!

                                                      1. 1

                                                        In that case I’ll also forgive being wrong in the ps about if vs when.

                                                        1. 1

                                                          Oh–would you explain further then? I always use when/when-let in the way I described (EDIT: and, I should add, many Clojure developers I’ve worked with as well assume this interpretation)–am I misunderstanding that semantically, fundamentally (from a historical lisp perspective or otherwise)?

                                                          1. 4

                                                            The ancient lisp traditions dictate that when you have two macros and one of them has an implicit progn (aka do) then that one should be used to indicate side-effects.

                                                            I know not all Clojure devs agree, but when you’re arguing that you should be less concerned than CL users about side-effects it’s probably time to reconsider your stance.

                                                            1. 1

                                                              Thanks!

                                                              EDIT: I want to add a question/comment too–I’m a bit confused by your mention of side-effects here, as in my suggestion to use when vs. if in these cases, I’m simply taking when at face value in the sense of it returning nil when the predicate term evaluates to false. I can see an argument that having an explicit else path with a nil return value may be helpful, but I guess my take is that…well, that’s what when (and the when-let variation) is for in terms of its semantics. So I guess I’m still missing the point a bit here.

                                                              …but nonetheless appreciate the historical note as I’m a reluctant lisper largely ignorant of most of this kind of ephemera, if you will forgive me perhaps rudely qualifying it as such.

                                                              1. 4

                                                                The typical Lisp convention is that you use when or unless only when the point is to execute some side-effecting code, not to return an expression, i.e. when means “if true, do-something” and unless means “if false, do-something”. This is partly because these two macros have implicit progn, meaning that you can include a sequence of statements in their body to execute, not just one expression, which only really makes sense in side-effectful code.

                                                                So while possible to use them with just one non-side-effecting expression that’s returned, with nil implicitly returned in the other case, it’s unidiomatic to use them for that purpose, since they serve a kind of signalling purpose that the code is doing something for the effect, not for the expression’s return value.

                                                                1. 3

                                                                  Thanks mjn, that helps me understand technomancy’s point better.

                                                                  Fully digressing now but: more generally I guess I’m not sure how to integrate these tidbits into my Clojure usage. I feel like I’ve ended up developing practices that are disconnected from any Lisp context, the community is a mix of folks who have more or less Lisp experience and the language itself is a mix of ML and Lisp and other influences.

                                                                  In any case, thank you and technomancy for this digression, I definitely learned something.

                                                    3. 3

                                                      Oh I don’t think this solves the nil problem completely, but it’s as good as you can get in Clojure. If you’re coming from Java and have no or little experience in an ML-ish language, then this conceptual jump from null to Nothing is a bit difficult, so this article was primarily written for those people (beginner/intermediate Clojurists).

                                                      Also you have to admit that, beyond Turing completeness, language features are an aesthetic. We can discuss the pros and cons of those features, but there is no One Right Way. A plurality of languages is a good thing - if there were a programming language monoculture we would have nothing to talk about at conferences and on internet forums :)

                                                      1. 6

                                                        Also you have to admit that, beyond Turing completeness, language features are an aesthetic. We can discuss the pros and cons of those features, but there is no One Right Way.

                                                        It sounds to me like you are saying that all ideas about PL are of equal worth, which I disagree with. One can have a technical discussion about PL without it ultimately boiling down to “that’s just, like, your opinion man”

                                                        1. 5

                                                          That’s not at all what I’m saying. It’s like in art, you can say that the Renaissance period is more important than the Impressionist period in art history, but that doesn’t mean Renaissance art is the “correct” way to do art. We can also have a technical discussion of artworks, but we weigh the strengths and weaknesses of the artworks against the elements and principles of art and how skillfully they are composed, we don’t say one artwork is correct and the other is incorrect. This is the basics of any aesthetic analysis.

                                                          Likewise in programming language design, we can discuss the technical merits of features, e.g. yeah Haskell has an awesome type system and I really enjoy writing Haskell, but that doesn’t mean Haskell is categorically better than Clojure. When you have a ton of legacy Java to integrate with (as I do), Clojure makes a lot of sense to use.

                                                          1. 3

                                                            PL criticism is done on utilitarian grounds, not aesthetic grounds. You acknowledge as much in your second paragraph when you give your reason for using Clojure. I guess you can look at PL as art if you want to but I don’t like that mode of analysis being conflated with what researchers in the field are doing.

                                                            1. 2

                                                              PL criticism is done on utilitarian grounds, not aesthetic grounds.

                                                              Why not both?

                                                              1. 1

                                                                When you’re trying to figure out which language is better, it’s not a question of aesthetics.

                                                                Though to be fair, “better” is really difficult to nail down, especially when people end up arguing from aesthetics.

                                                    1. 1

                                                      As far as I can tell, this tool only analyzes PHP code.

                                                      1. 9

                                                        This is a good write-up. I often find with Clojure, because it’s so flexible but also because it’s dynamically typed (which is good and bad) that it takes a bit of mental legwork to get to the point where I feel like I’m using it correctly. I also haven’t been using it continuously or following the community, and best practices change so fast. It’s great to be able to get some insight from someone who’s been using it in production for 7 years.

                                                        Has anyone (I imagine the OP hasn’t) tried out Typed Clojure? How has that worked out for people?

                                                        1. 9

                                                          FWIW, the author of the piece is the (a?) founder of CircleCI, which had one of the most extensive integrations with Core.typed in existence (I believe) up until last fall: https://circleci.com/blog/why-were-no-longer-using-core-typed/

                                                          1. 3

                                                            Typed Clojure is cool, but it has a ways to go. Gradual typing is a work in progress at present, and I haven’t played with the stuff that’s in development right now, so when we type a library for internal consistency that works pretty well, but dealing with inference on Other People’s Code is more difficult.

                                                            Matthias Felleisen did a talk at Clojure/West about the benefits of type systems in dynamic language and outlined a few of the shortcomings of core.typed.

                                                            That said, Schema is a nice happy medium, but necessitates that you pair schema definitions with tests most of the time (and/or, run validation while you’re developing). Runtime validation is still pretty useful, and thinking about the shape of your data is always a benefit.

                                                          1. 39

                                                            …this is a critique of fp that is both informed and aims to be balanced.

                                                            It’s not. It’s trivial and insipid, hinging its main argument on the claim that “The syntax of functional programming just isn’t readable at a glance.” Whether or not code is readable at a glance is really dependent on a vast number of factors, and furthermore the author expects that we accept, unequivocally, this vague notion of “readability” being a critically important factor in choosing a language. I don’t always agree with Martin Fowler but I doubt he’d approach the argument with the same lack of nuance. Quoting him to bolster the point is an appeal to authority which doesn’t even hit the mark.

                                                            It’s easy to find more to criticize in this piece, but more generally I think we should stop with these OO vs. Functional programming articles–the reality is that it’s much more illuminating and meaningful to talk about specific choices in a given language or languages and compare and contrast what programming styles they enable or promote, or what therefore becomes difficult because of specific choices. I like reading about philosophies underpinning the structure of a language or the history of a language feature…etc. I agree with what sdiehl wrote: there are so many interesting dimensions to discuss–why bother with a piece like this?

                                                            1. 10

                                                              It is hilarious as well, since the author quotes Quicksort in Erlang as hard to read whereas Quicksort in an imperative language like C is even worse to read and understand and most importantly, to get right. The argument is completely flawed.

                                                              1. 2

                                                                While I don’t disagree with you about the problems in the article, I do feel like that C implementation of quicksort you linked to as an example of poor readability has readability problems primarily because of extremely poor variable naming, something which is easy to fix (though that’s perhaps not the only problem, but it is the one that will stand out the most to people). That kind of poor naming seems to be ingrained in C programming culture which is unfortunate. I write C code myself and I always use descriptive names anyway. I’m also not really sure I agree that implementing quicksort in C is hard to get right, at least not moreso then with most other imperative languages.

                                                                1. 1

                                                                  I think the readability problems with the functional implementations are primarily because of extremely poor function naming, which unfortunately seems ingrained in Haskell programming culture.

                                                                  1. 2

                                                                    I doubt the names are poor, but rather, unfamiliar to a general audience. Most programmers don’t understand the theory from which they originate.

                                                                    1. 1

                                                                      No, I think they’re badly named even within the theory. Too many symbols that would be better as words, and too much inconsistency just because of traditions in the field (e.g. “type constructor”).

                                                              2. 7

                                                                “The syntax of functional programming just isn’t readable at a glance.”

                                                                I really hate syntax arguments. They hide systematic arguments.

                                                                For example, good object-oriented systems have the habit that everything happens somewhere else. This can be extremely confusing and hard to grasp at a glance (though not necessarily bad, systematically thinking). It has nothing to do with syntax, though.

                                                                1. 4

                                                                  Take my python fan advice as it is.

                                                                  I think readability is not a critically important factor, but it is important as a lots of others. One of other important dimension is as believe how much immutability of data structures there is built in language (and here python is really poor)

                                                                1. 4

                                                                  This article is simply adding more confusion to the current state of affairs regarding FRP.

                                                                  Don’t get me wrong–I don’t subscribe to the school of thought that only the creator of a thing gets to determine its path and definition, but reading through an article on FRP without once seeing a reference to any of the substantial work done in this area is infuriating.

                                                                  If you’re going to talk about a thing and proclaim some expertise on it to the degree that you think you can explain it, take (author of Elm) Evan Czaplicki’s lead and do your homework without complaining about “the rabbit-hole of monads, functors and other technical jargon.” That “technical jargon” is in there because people actually worked hard to describe something concretely in terms that allowed for a specific understanding and implementation of the concept.

                                                                  Here are some good resources to start with if you actually want to learn about some of the different formulations of FRP that aren’t just hand-wavy:

                                                                  http://begriffs.com/posts/2015-07-22-essence-of-frp.html (Conal Elliott describes his conception of FRP)

                                                                  http://stackoverflow.com/a/1030631 (ditto, but in an Stack Overflow answer)

                                                                  http://elm-lang.org/papers/concurrent-frp.pdf (good description of precedents and various tradeoffs in the beginning)

                                                                  https://www.youtube.com/watch?v=Agu6jipKfYw (talk on FRP by Elm creator)

                                                                  http://cs.brown.edu/~sk/Publications/Papers/Published/mgbcgbk-flapjax/paper.pdf (JS implementation of FRP informed by precedents)

                                                                  1. 2

                                                                    I thought this article was much more approachable than Conal Elliott’s talk. dmvaldman made FRP into a pretty relatable concept, for me anyhow. Elliott’s, on the other hand, was quite technical.

                                                                    1. 1

                                                                      I don’t debate that the linked piece is more approachable…but how does it relate to Conal Elliott and Paul Hudak’s original formulation of FRP or any of the formulations that came after that in response?

                                                                    2. 1

                                                                      without once seeing a reference to any of the substantial work done in this area is infuriating.

                                                                      Is everything that mentions a thing required to cite everything it’s ever built upon?

                                                                      1. 2

                                                                        Is everything that mentions a thing required to cite everything it’s ever built upon?

                                                                        No, of course not, but please allow me to try to persuade you that the author is failing to connect their concept of FRP to the original notion and the formulations which came after, and being confusingly vague otherwise, at the expense of the reader’s understanding.

                                                                        For example, the author makes the statement in the “Implementation and Design” section that

                                                                        This is fundamental to FRP: no computation is wasted on changes that are not observed. You get performance for free.

                                                                        They seem to be implying that this has some relationship to how the ordering of events in physical reality is inverted when implementing ray tracing, and this has implications for performance. However, I don’t feel like I have any understanding of why this is; the author doesn’t explain this other than to imply that because ray tracing is more performant (than simulating reality I assume, since as far as I understand ray tracing’s strength is not in how performant it is but in how realistic its results are), and the ordering of events in FRP is analogous to the ordering of events in raytracing, therefore we get performance for free in FRP.

                                                                        However, even leaving aside the problematic aspects of the above statement, if you read Conal Elliott’s summary on stackoverflow which I linked to in my previous post, you’ll note that he ends with

                                                                        It’s been quite a challenge to implement this model correctly and efficiently, but that’s another story.

                                                                        Further investigating, if you read Evan Czaplicki’s paper on Elm, you’ll note that he talks about the challenges of implementing FRP using continuous semantics vs. discrete, and talks about how

                                                                        Concurrent FRP solves some of FRP’s long-standing efficiency problems: global delays and needless recomputation.

                                                                        and furthermore

                                                                        Unnecessary updates are caused by discrete inputs, such as mouse clicks. Continuous signals assume that signal values are always changing.

                                                                        …etc–he goes on with a thorough analysis, and Elm itself is in fact (in part) an attempt to mitigate some of these issues. And if you follow the thread you’ll note that Conal Elliott re-formulated FRP in his paper Push-Pull FRP to address issues of efficient implementation. The first statement in the abstract is

                                                                        Functional reactive programming (FRP) has simple and powerful semantics, but has resisted efficient implementation.

                                                                        So, how does this square with the original piece’s author’s claim that in FRP “no computation is wasted on changes that are not observed. You get performance for free?” I have no idea–it sounds to me as though the author is talking about something completely different than what Elliott and Czaplicki are talking about. I see only the most tenuous connection between what the author describes and the original conception of FRP.

                                                                        Therefore I hope you’ll understand why I find this frustrating and why I think it’s bad for people trying to understand what FRP actually is–in the end, as someone who himself has struggled to understand FRP and trace its lineage and development through different stages, I find that this piece is muddying the waters, not illuminating anything.

                                                                        I know that it’s unreasonable to expect a term to be used consistently once it enters the general collective consciousness of the programmer-culture/industry matrix: terms get twisted and diluted to suit a easy-to-understand or profitable version of that thing. Look at the term “object-oriented programming” for the perhaps canonical example of this.

                                                                        But I hope I can at least be forgiven for pointing out the inconsistencies here as I see them. I think we would benefit, as a community, from consistently applying critical thinking to pieces like this.

                                                                        (edited for grammar)

                                                                    1. 2

                                                                      I was only able to read the summary of the article the post was responding to, but after watching Erik’s presentation about how Agile is complete shit and should die (I’m paraphrasing, but I think it’s a reasonable summary: http://vimeo.com/110554082), I can guess that many of the points being made are one and the same.

                                                                      I recently took the edX Intro to Functional Programming course Erik led, and found him to be an incredibly talented computer scientist, programmer, and educator, and I really love his philosophy regarding programming languages. But he tends towards the hyperbolic and I think he is throwing out the baby with the bathwater here (I can’t speak to his co-author Vikram Kapoor, again I’m more responding to the video than the referenced article).

                                                                      There is a lot of dogmatism (and, cynically, money to be made off of that) when it comes to software development process and in particular in the culture of so-called Agile development. But there is also a lot of value there, and the Agile manifesto, for all of the bullshit it has spawned, remains a succinct and reasonable set of guiding principles for doing software development. If anything it’s too vague to really be considered prescriptive of any one practice (and ironically it does eschew process explicitly in the first statement).

                                                                      What happens when you let “first-rate hackers” go to town and get stuff done? Well, if they are not dysfunctional, hopefully they–and the rest of the members of the team who aren’t programmers, assuming the software is intended to be used by people–create some semblance of a process. The problem is when those practices become ossified and we become slaves to them, which I think is what Erik is so dramatically railing against in the video I linked to above.

                                                                      I understand his perspective, but I think the truth is somewhere in between. The challenge is to build a process that provides useful structure without being afraid to modify or challenge it when it’s obviously not working. This is difficult, but the alternative–no structure whatsoever–is even harder to deal with.