1. 2

    I’m writing a response to this post right now. It’s longer so I’ll put it into my blog.

    The last one I did send here ended up being merged on without a bump-up on the original post. I guess the same happens now and the thread will be buried 4 pages down the board before I’m ready to send it.

    But I guess I’ll send it in anyway.

    1. 2

      I’d be curious what you think of this talk.

      It includes clips of interviews from decades ago from some of the originators of OOP as we know it today, talking about what their goals were and how they aimed to achieve them.

      https://youtu.be/6YbK8o9rZfI

      1. 1

        There’s that thing again.. It’s more of a wording issue but eventually translates into a thinking issue. Functional programming doesn’t erase or “remove” side effects. It just separates side effects from reduction rules.

        Also after I watched this video and thought on it a bit, a question arose: Is the modern stuff OOP at all? Has the definition been extended to cover every use of class-like structures?

      1. 5

        Just gonna take one potshot at the new merged article:

        Are you sure about polymorphism? First of all you took it from type theory and that’s itself getting popular featuring stable forms of parametric polymorphism, while your version of polymorphism is shifting shape like crazy.

        You have it backwards. ML cited CLU’s type system as inspiration, which cited SIMULA as inspiration. Parametric polymorphism may be more elegant in typed FP, but from a historical engineering perspective it migrated from OOP to FP.

        1. 1

          Type theory itself is older than either ML or Simula, but it’s worthwhile to add this in. I’ll put a note in there sometime soon.

        1. 1

          I used to really hype over MLsub but eventually moved on to hype over type theory and category theory. The MLsub inference algorithm was indeed quite complex. I guess I’ll take a look at this and review the paper in my blog.

          MLsub is still interesting but for a different reason though. When I’m coding Idris or Haskell, awful often it seems like everything should be a typeclass. This is also presenting up in category theory. Adjoint functors seem to form “arguments” that introduce evaluation rules as well, resembling typeclasses. MLsub constraints resemble typeclasses.

          1. 8

            Gets it wrong. Title ends up being ironical as the concept is buried into bunch of rustica.

            Wadler’s paper doesn’t explain much of what a monad is. He shows that it can be used for sequencing side effects in a functional programming language.

            Monad is a functor with two natural transformations that are coherent in a certain way.

            The text entirely misses the idea that it’s a functor.

            1. 9

              So IDEs are merging with compilers? Just 2 days ago I wrote about similar stuff, except in context of compilers and build systems. Olle Fredriksson wrote about query based compilers before that.

              1. 3

                Seems queryable AST idea become a trend recently. There is another article the other day: https://nshipster.com/as-we-may-code/ Curious what drives the explosion of these thoughts.

              1. 1

                I think it’s nonsense. It confuses abstraction and organization.

                Abstractions select details you are treating. Shifting volume of a sphere into a function is not abstraction because you still care about what the formula is.

                The same repeats in the “bloated method”. It reorganizes things but details do not change, nothing is abstracted away.

                “Awkward class”, same thing. Still talking about shapes but splits classes. Code is reorganized, not changing abstraction.

                The final example is just different expansions of the same thing. It’s like dung beetle rolling pieces around. The “pushback in a PR review” feels like a cherry on top. This is silly.

                1. 2

                  I think you might be taking the definition of abstraction to be a bit different than what is intended. Note that “lambda abstraction” is a technical term, and is the primary approach to building “Abstractions” in code.

                  Yes, this is all just code reorganization. The important point is that some of it is helpful for maintaining code, while some of it is detrimental. In my opinion, this blog post does a wonderful job of providing insight into when and why you should use “abstraction”, and, more importantly, when and why you should not.

                1. 3

                  The premise in the post is good but functional promoting posts are almost always silly. This one isn’t an exception.

                  You got this misconception that functional programming is about maintaining state. The people then answer to you that state is needed anyway. But that’s not the point because this maintaining state -viewpoint kind of misses the point because it’s too narrow.

                  Types encode the known behavior of the program. If we take the recipe example in this thing, your type might be:

                  2 eggs 
                  ⊸ 2 cups flour
                  ⊸ Oven heated at 400F degrees
                  ⊸ kitchen equipment
                  ⊸ 60 minutes
                  ⊸ (10 cupcakes ⊗ oven ⊗ kitchen equipment)
                  

                  The proof would contain the recipe, the strategy you use to produce 10 cupcakes and not lose the oven and your other equipment. You use the logic-correspondence of the types to present the problem. For convenience I used linear types here because it’s a kitchen recipe. I assumed your tools don’t break when you use them.

                  Now consider how much easier it’s to navigate through a program where it’s explicit what we are expecting from it, and written such that the program cannot violate the expectations, at least not completely?

                  Of course this means you need pure and typed, and maybe even non-turing complete language, but I think functional as-it is just hype.

                  1. 3

                    Hmmm. As someone with zero professional functional programming experience, I thought the post was pretty good at explaining why I might be interested in learning more about the paradigm.

                    To be honest, I am not sure I understand your comment–some of the terms are new to me :) .

                  1. 0

                    Going back to using typed languages is a downgrade from Python if you don’t have knowledge about dependent typing.

                    Also if your language enforces compile from scratch/run cycle, it sucks just like Python sucks when it does that, precisely because it’s interpreted but not properly typed language.

                    1. 9

                      Great news! I am eager to try this!

                      Turn on -XLinearTypes, and the first thing you will notice, probably, is that the error messages are typically unhelpful: you will get typing errors saying that you promised to use a variable linearly, but didn’t. How hasn’t it been used linearly? Well, it’s for you to puzzle out. And while what went wrong is sometimes egregiously obvious, it can often be tricky to figure the mistake out.

                      So, basically, GHC just got its own “Syntax error” a la OCaml… just a bit more specialized :p.

                      1. 11

                        Maybe it’s just me, but to me OCaml’s errors are terse and unhelpful and GHC’s errors are… verbose and unhelpful. ;)

                        There are interesting papers that show working ways to improve both, but I wonder why none of those improvements are in the mainline compilers.

                        1. 2

                          Good error reporting is easiest if it’s built into the compiler front end from the start. If a new algorithm comes along to improve the error information it’s almost never going to be a simple job to drop it into an existing compiler.

                          You need type information & parse information from code that’s potentially incorrect in both spaces, so any error algorithm usually has to be tightly integrated into both parts of the compiler front end. That tight integration usually means that improving compiler errors is a significant amount of work.

                          1. 3

                            It varies. What puzzles me is that a lot of time ready to use, mergeable patches take much longer to merge than they should.

                            Like this talk: https://ocaml.org/meetings/ocaml/2014/chargueraud-slides.pdf

                            1. 1

                              Do you also have a link for a patch for the improved error messages?

                              A lot of work has been going on to move OCaml to a new parser and improve error messages. Even though there is a lot still needed to be done, latest releases have started improving a lot. Maybe we can still extract some useful bits from that effort and try again

                              1. 2

                                Turns out it was even made into a pull request that isn’t merged yet: https://github.com/ocaml/ocaml/pull/102

                                1. 1

                                  Thanks. It is quite an informative PR actually, and explains why the change is not there yet and once can infer why it is easier to add informative messages in new languages and complier but it may be quite hard to retrofit them to seasoned ones

                        2. 7

                          Would you be kind enough to give me an ELI5 about what linear types are and what you can do with them?

                          1. 29

                            In logic, normal implication like A implies B means whenever you have A, you can derive B. You have tautologies like “A implies (A and A)” meaning you can always infinitely duplicate your premises.

                            Linear implication is a different notion where deriving B from A “consumes” A. So “A linearly implies B” is a rule that exchanges A for B. It’s not a tautology that “A linearly implies (A and A).”

                            The classic example is you can’t say that “$3 implies a cup of coffee” but “$3 linearly implies a cup of coffee” makes sense. So it’s a logical form that reasons about resources that can be consumed and exchanged.

                            Same in functional programming. A linear function from type A to type B is one that consumes an A value and produces a B value. If you use it once with an A value then you can’t use it again with the same A value.

                            This is nice for some performance guarantees, but also for encoding safety properties like “a stream can only be read once” etc.

                            1. 6

                              Keynote: Linear Haskell: Practical Linearity in a Higher-Order Polymorphic Language https://skillsmatter.com/skillscasts/11067-keynote-linear-haskell-practical-linearity-in-a-higher-order-polymorphic-language

                              1. 5

                                It can be used to model protocols with type signatures. The following is in theory what you should be able to do.

                                data ConsoleInput
                                    = Input String ConsoleOutput
                                    | ExitInput
                                
                                data ConsoleOutput
                                    = PrintLines ([String] ⊸ Console)
                                    & PrintLastLines ([String] ⊸ ())
                                
                                greet :: ConsoleOutput ⊸ ()
                                greet console
                                    = let PrintLines f = console
                                      in step2 (f ["name?"])
                                
                                step2 :: ConsoleInput ⊸ ()
                                step2 ExitInput = ()
                                step2 (Input input console)
                                    = let PrintLastLines f = console
                                      in f ["hello " ++ input]
                                

                                If you combine it with continuation passing style, you get classical linear logic and it’s a bit more convenient to use.

                                If you model user interfaces with types, they should be quite useful.

                                I’m also examining and studying them: http://boxbase.org/entries/2020/jun/15/linear-continuations/

                                1. 1

                                  Wikipedia gives a reasonable overview. The closest analogy would be something like move semantics – for example ownership in Rust can be considered as manifestation of linear types.

                                  1. 6

                                    Rust ownership is linear affine types. Linear types are similar but differ in the details. A shitty way of understanding it is affine types mimic ref counting and prevent you from having a ref count < 0. Linear types are more a way of acting like RAII in that you might create a resource but just “know” that someone later on in the chain does the cleanup.

                                    Which I’m sure sounds similar but affine types allow for things like resource leaks but linear types should guarantee overall behavior to prevent it.

                                    This all assumes my understanding and explanation is apt. I’m avoiding a ton of math and i’m sure the shitty analogy doesn’t hold up but behaviorally this is how I have it in my brain.

                                    1. 2

                                      Linearity Design Space: https://i.imgur.com/s0Mxhcr.png

                                      1. 2

                                        I’m personally of the stance that the 2020 linear ghc stuff is more <= 1 usage, and kinda misses out on a lot of really fun expressivity that can fall out of making full classical linear logic first class. But that’s a long discussion in its own right , and I’ve yet to make the time to figure out the right educational exposition on that front

                                        1. 1

                                          it definitely seems more limited in scope/ambition compared to the effort ongoing for dependent types, for better or worse. Can’t say I know much about what first class linear logic would look like, but perhaps now there will be more discussion about such things.

                                          1. 2

                                            The really amazing thing about full linear logic is it’s really sortah a rich way to just do mathematical modelling where everything has a really nice duality. The whole thing about linearity isn’t the crown jewel (though wonderfully useful for many applications ), it’s that you get a fully symmetric bag of dualities for every type / thing you can model.

                                            The paper that really made it click for me was mike shulmans linear logic for constructive mathematics paper. It’s just a fun meaty read even at a conceptual level. There’s a lot of other work by him and other folks that taken together just point to it being a nice setting for formal modelling and perhaps foundations of category theory style tools too!

                                        2. 1

                                          Not sure I can agree that Uniqueness types are the same as Linear types. Care to explain they’re similar sure but not the same thing and your… screenshot of a powerpoint? isn’t very illustrative of whatever point you’re trying to make here.

                                          And from my experience with Idris, I’m not sure I’d call what Rust has Uniqueness types.

                                          1. 1

                                            They are different rows in the matrix because they are different, of course.

                                            it’s from this presentation about progress on linear ghc a little over a year ago https://lobste.rs/s/lc20e3/linear_types_are_merged_ghc#c_2xp2dx skip to 56:00

                                            What is meant by Uniqueness types here is “i can guarantee that this function gets the unique ptr to a piece of memory” https://i.imgur.com/oJpN4eN.png

                                  2. 2

                                    Am I the only one thinking this is not how you ship language features?

                                    If the compiler can’t even report errors correctly, the feature shouldn’t ship.

                                    1. 15

                                      If the compiler can’t even report errors correctly, the feature shouldn’t ship.

                                      Its more this is an opt-in feature with crappy error reporting for now using computer programming design features not in use in most programming languages. Its going to have rough edges. If we required everything to be perfect we’d never have anything improved. Linear types like this also might not have a great way to demonstrate errors, or the domain is new so why not ship the feature for use and figure out what kind of error reporting you want based on feedback.

                                      1. 13

                                        Many people do not realize that haskell is a research language and GHC is one of the main compilers for it. This is an experimental feature in a research language. If it works out well, then it will be standardized.

                                      2. 5

                                        Other people have sort-of said it, but not clearly enough I think. This is not a language feature being added. It is a feature-flagged experimental feature of a particular compiler. Most such compiler extensions never make it into real Haskell, and the ones that do take years after they are added to a compiler to make it to a language spec.

                                        1. 4

                                          for all practical purposes isn’t “real Haskell” defined by what ghc implements these days?

                                          1. 2

                                            Yes, all the other implementations are dead. They still work, but they won’t run most modern Haskell code, which usually uses a bunch of GHC extensions.

                                            1. 1

                                              You might say “isn’t it not popular to write standards-compliant Haskell these days?” and you’d be right. Of course it’s often trendy to write nonstandard C (using, say, GNU extensions) or nonstandard HTML/JavaScript. However, ignoring the standard being trendy doesn’t mean the standard doesn’t exist, or even that it isn’t useful. I always make sure my Haskell is Haskell2010, and I try to avoid dependencies that use egregious extensions.

                                            2. 2

                                              Honestly curious: are there any other Haskell compilers out there? Are they used in production?

                                              Also, what is a definition of a true Haskell? I always thought it’s what’s in GHC.

                                              1. 5

                                                There’s a Haskell which runs on the JVM - Frege. But it makes no attempt to be compatible with the version of Haskell that GHC impements, for good reasons. Hugs is a Haskell interpreter (very out of date now, but still works fine for learning about Haskell.) There a bunch of other Haskell compilers, mostly research works that are now no longer in development - jhc, nhc98 etc etc.

                                                But GHC is the dominant Haskell compiler by far. I don’t think there are any others in active development, apart from Frege, which isn’t interested in being compatible with GHC.

                                                (“True Haskell” is the Haskell defined in the Haskell Report, but real world Haskell is the Haskell defined by what GHC + your choice of extensions accepts.)

                                                1. 2

                                                  There are other compilers and interpreters. None of them is anywhere near as popular as GHC, and usually when one does something interesting GHC consumes the interesting parts.

                                                  There is definitely a standard, though: https://www.haskell.org/onlinereport/haskell2010/

                                                  The whole reason language extensions are called “extensions” and require a magic pragma to turn on is that they are not features of the core language (Haskell) but experimental features of the compiler in question.

                                                2. 1

                                                  In short, GHC Haskell is a language designed by survival-of-the-fittest.

                                                  1. -2

                                                    If you haven’t noticed, the language spec is dead.

                                                  2. 3

                                                    Overly terse error messages are bad, but they are better than wrong error messages. Some things are much harder to give helpful error messages for than others.

                                                    I wish people spend more time improving error reporting, at least in cases when the way to do it is well understood. There is no reason for say TOML or JSON parsers to just say “Syntax error”. But, YAML parsers are pretty much doomed to give unhelpful errors just because the language syntax is ambiguous by design.

                                                    And then some errors are only helpful because we know what their mean. Consider a simple example:

                                                    Prelude> 42 + "hello world"
                                                    
                                                    <interactive>:1:1: error:
                                                        • No instance for (Num [Char]) arising from a use of ‘+’
                                                        • In the expression: 42 + "hello world"
                                                          In an equation for ‘it’: it = 42 + "hello world"
                                                    

                                                    How helpful is it to a person not yet familiar with type classes? Well, it just isn’t. It’s not helping the reader to learn anything about type classes either.

                                                    1. 1

                                                      I’ve seen some good suggestions on r/haskell for improving the wording of these errors.

                                                    2. 2

                                                      The error they’re talking about is a kind of type error they’ve not worked with. It’s produced if you forget to construct or use a structure. I I’m guessing it’s technically “proper” but the produced error message may be difficult to interpret.

                                                      They’ve ensured it’s a feature you can entirely ignore if you want to. Everybody’s not convinced they need this.

                                                      I otherwise dunno what they’re doing and I’m scratching my head at the message. Something like “Oh cool you’ve done this… … … So where are the types?”

                                                      1. 2

                                                        So you never got a C++ template error in the good olden days? Seriously though, it just got merged. It’s not released or “shipped” in any means.

                                                        1. 0

                                                          So you never got a C++ template error in the good olden days?

                                                          No, because I looked at the language, figured out that the people involved completely lost their fucking mind, and moved on.

                                                          Seriously though, it just got merged. It’s not released or “shipped” in any means.

                                                          They took 4 years to arrive at the current state, which I’ll approximate at roughly 10% done (impl unfinished, spec has unresolved questions, documentation doesn’t really seem to exist, IDE support not even on the radar).

                                                          So if you assume that there will be a Haskell version in the next 36 years, then this thing is going to end up in some Haskell release sooner or later.

                                                          1. 2

                                                            So if you assume that there will be a Haskell version in the next 36 years, then this thing is going to end up in some Haskell release sooner or later.

                                                            Could you elaborate on this? If practical users of linear types will only use them if they have good error messages, and early testers want to work out the kinks now, what’s wrong with having a half-baked linear types feature with no error messages permanently enshrined in GHC 8.12?

                                                    1. 4

                                                      Could you throw some sack on Lever? This is author’s request, you will be welcome to do that.

                                                      1. 2

                                                        I can try!

                                                      1. 27

                                                        I am a very vocal person about systemd.

                                                        With that said, I find the articles opening statements to be quite fair in all regards- However, it does seem to fall right into the trap of comparing systemd with sysvinit.

                                                        Without fail whenever someone extolls virtues and waxes poetic on the subject of systemd being “easy” it is always with the preconceived notion that sysvinit was the alternative.

                                                        My typical argument against systemd is precisely in that it’s so complex and ever changing as to be effectively a closed system; integration with systemd specifics ensures that there can never be a successor, we are locked in to systemd forever more. So it had better be damned near perfect architecturally; and given the compromises and nature of exploits I do not have faith that it is.

                                                        evidenced by the lengthy but great run down of the events surrounding systemd and the recent system-down exploit.

                                                        There were alternatives; runit being a good example, but now swapping out your init for any other will cause many bits of useful software (notably gnome, but more as time goes on) to no longer work.

                                                        1. 13

                                                          Even without comparing it with anything. What valid reason is there to increase the scope of what is generally percieved as an init system by some 1000%?

                                                          I, like most people, like being able to define services just by creating a 4 line unit file rather than having those silly ad hoc old init scripts. But why not doing just that? Why do I all of the sudden need an application just to check logs?

                                                          1. 5

                                                            Counterpoint: with systemd the logs are now collected in a pretty consistent way instead of spread eccentrically around /var/log. I find it convenient that I don’t need to define what my logfiles are going to be called or what rotation will occur, etc. Those aren’t interesting problems to me.

                                                            1. 5

                                                              the logs are now collected in a pretty consistent way

                                                              Few adjectives are missing: error-prone, inefficient, insecure. You can insert them after pretty and then your sentence is 100% spot on.

                                                              Links:

                                                              If you ask me if I want to trade the previous logging stack to this, I would say no.

                                                              1. 2

                                                                That’s your prerogative. I have not experienced any of those issues but have experienced issues with the traditional approach. In any case, all systems have faults.

                                                          2. 7

                                                            Most likely a system such as systemd is not correct or even possible to implement correctly because they’re not doing any formal verification to ensure every D-bus component conforms to their agreed upon interface.

                                                            I’m just waiting for a cardhouse to fall.

                                                            1. 2

                                                              many bits of software, you mean

                                                              1. 1

                                                                Did any distros actually use runit?

                                                                I used daemontools for a tiny project for awhile, and while it’s very elegant, I got the sense that you were also swimming upstream. It was more a thing I used on top of a distro (on top of sysv init or systemd), not really a replacement.

                                                                I don’t know the details but I’d hope and expect that the projects that integrate with systemd would accept patches for non systemd inits.

                                                                1. 8

                                                                  Did any distros actually use runit?

                                                                  Void uses it, Artix recently added it as an option, and Project Trident recently switched over to Void as their base so I believe they also use Runit now too.

                                                                  I don’t know the details but I’d hope and expect that the projects that integrate with systemd would accept patches for non systemd inits.

                                                                  I doubt it. Most programs I’ve seen that depend on systemd often have it as a hard requirement. Which forces users to use tools like elogind or consolekit or eudev, or patch the program themselves to not use systemd. It’s a trivial thing when using a distro like Gentoo, but I’m sorry for those who use distros like Debian because it’s near impossible to escape from systemd there.

                                                              1. 1

                                                                My sister likes Nix and I’m probably going to update on it as well. Ubuntu updates are too stressful and resemble fresh installs because so much stuff always breaks between them. Formatting a disk and throwing Nix config on it before I’ll need next upgrade due to third-party support moving away from LTS version pre-officially.

                                                                1. 6

                                                                  I mostly agree but I would say that OO is evolving toward FP and they aren’t that far apart (at least in certain circles, maybe not in 20 year old code).

                                                                  Good OO is also principled about state and I/O, just like FP.

                                                                  Related comment I wrote on “How to Avoid the Assignment Statement”

                                                                  https://news.ycombinator.com/item?id=22835750

                                                                  Older comment on “Disadvantages of Purely Functional Programming”

                                                                  https://news.ycombinator.com/item?id=11841893

                                                                  So while I agree with the general gist, I would add the caveat that it’s easier to do FP in OO languages than OO in functional language. And the former is pretty natural.


                                                                  Another way I think about it is “dependency inversion of state and I/O”, which I wrote about here:

                                                                  http://www.oilshell.org/blog/2020/04/release-0.8.pre4.html#dependency-inversion-leads-to-pure-interpreters

                                                                  And I also think in many programs it’s useful to “cheat” a little to get it working, and then refactor to a more principled architecture.

                                                                  1. 9

                                                                    There’s a fundamental difference between OO and FP. With FP the core idea is to keep data and code separate. Programs are written as pipelines of functions that you pass data through, and get another piece of data at the other end. In modern FP, data is also immutable, so the functions are referentially transparent. The big advantage here is that data is inert by nature because it doesn’t have any behaviors associated with it, and it’s transparent. What you see is what you get because it’s just data.

                                                                    And you operate on this data by combining a common set of functions from the standard library. Once you learn how these functions work, it becomes trivial to transform any kind of data using them. It’s akin to having a bunch of Lego pieces that you can put together in many different ways to accomplish different tasks.

                                                                    On the other hand, objects are state machines that have some internal state with the methods as the API for interacting with that state. An application written in OO style ends up being a graph of opaque interdependent state machines, and this tends to be quite difficult to reason about. This is why debugging is such a big part of OO development. It’s impossible to reason about a large OO application because there’s just too many things that you have to keep in your head. So, the only thing you can do is put a break point, get the application in the desired state, and try to figure out how you got there.

                                                                    Meanwhile, classes are really ad hoc DSLs, each class defines its own custom API and behaviors in form of its methods, and knowing how one class works tells you absolutely nothing about the next class. The more classes you have in your program, the more stuff you have to keep in your head to work with it effectively.

                                                                    This also makes it much more difficult to learn APIs for libraries. When you call a method, and you get an object graph back, now you have to learn about each of these objects, and how they behave. When your API is data driven, this problem doesn’t exist. You call a function, get some data back, and that’s the end of the story.

                                                                    Rich Hickey describes this problem here very well, and that matches my experience very closely.

                                                                    1. 6

                                                                      No, that is sort of an old way of thinking about OO. There are a lot of people who don’t write it that way, including me. You can do something much closer to FP in a OO language.

                                                                      That is the point of the linked comments. Exceprt:

                                                                      I use “functions and data” (a la Rich Hickey).

                                                                      Except my functions and data are both CLASSES. Data objects are basically structs, except they can do things like print themselves and answer simple queries based on their values, which helps readability (e.g. 1 liners, like word.AsFuncName() ).

                                                                      Function objects are simply classes with configuration passed to constructors. That usually have a single method, but multiple methods are also often useful. Calling this method is basically equivalent to calling a curried function. But this is supremely useful for both compilers and servers, because often you have config/params that is constant once you reach main(), and then you have params that vary per request or per file processed. Many functions depend on both, and it’s cleaner to separate the two kinds of params.

                                                                      So both “functions and data” are effectively and usefully implemented as classes.

                                                                      The Oil interpreter started out maybe 60% in this style, and is approaching 90% of that style. It’s tens of thousands of lines of code, so it’s not a small change.

                                                                      There are a few classes that are state machines, but they are explicitly limited to about 10% of the interpreter, just as you would do in a functional language. Most of it is parsing, which has “local” mutable state inside and an interface that’s basically a function.

                                                                      Again, from the comments, the thing I found funny is that for lexing and parsing, languages like OCaml just borrow the exact same mutable algorithms from C for lexing and parsing (LALR parsing and DFAs for regexes). The mutable escape hatch of of OCaml is essential.

                                                                      Lexing and parsing are inherently stateful. As long as it’s localized, it’s no problem. FP and OO both agree on that.

                                                                      1. 1

                                                                        Thing is that in practice you rarely pass functions around in Clojure. Vast majority of the time you’re passing around plain data, you pass that data through different functions to transform it, and get new kind of data back. There is very little reason to pass functions around in my experience. So, yes you can do similar stuff to OO with functions and closures, but that’s not generally how you end up structuring your applications.

                                                                        And yes, you can write FP style code in OO languages, but then you’re really not making the most out of the paradigm the language was designed for. You’re much better off doing FP in an actual FP language.

                                                                      2. 3

                                                                        This also makes it much more difficult to learn APIs for libraries. When you call a method, and you get an object graph back, now you have to learn about each of these objects, and how they behave. When your API is data driven, this problem doesn’t exist. You call a function, get some data back, and that’s the end of the story.

                                                                        It creates another problem though: exposing implementation details. Your clients may start assuming things about the data structure that need to be changed. This article tells the story of an API that exposed a list [1]. It turned out the list was too slow, and they wanted to change it. Unfortunately the API’s clients assumed it was a list. The solution given in the article? Hide the data behind a constructor and selectors. That’s basically a class definition.

                                                                        1: https://itnext.io/information-hiding-for-the-functional-programmer-b56937bdb789

                                                                        1. 2

                                                                          Not really because I choose what data I return from the API functions in a library. Meanwhile, your anecdote could’ve happened just as easily using OO API. In fact, I would argue that it’s a far more common problem in OO since you return a graph of objects, and if you ever need to change any of them down the line you’ll be breaking the API for all your users.

                                                                          Having been working with Clojure for around a decade now, I can tell you that this problem has yet to come up for me in practice.

                                                                          1. 1

                                                                            In fact, I would argue that it’s a far more common problem in OO since you return a graph of objects, and if you ever need to change any of them down the line you’ll be breaking the API for all your users.

                                                                            One of the core tenets of OOP is to program to the interface, not the implementation. If you change the implementation but keep the interface unchanged, you are guaranteed to not break the downstream consumers.

                                                                            1. 1

                                                                              Interfaces only address the problem partially, because if the interface ever changes that will still create a breaking change. My experience is that changes to interfaces in OO happen far more regularly than changes to the shape of the data in functional APIs.

                                                                              1. 1

                                                                                As per the Java Language Spec,[1]

                                                                                …here is a list of some important binary compatible changes that the Java programming language supports: … Adding new fields, methods, or constructors to an existing class or interface.

                                                                                (Among others)

                                                                                This is similar to making an additive change to the shape of data, e.g. adding a new field to a map which is consumed by a function that doesn’t use the field.

                                                                                [1] https://docs.oracle.com/javase/specs/jls/se7/html/jls-13.html

                                                                                1. 1

                                                                                  Having worked with Java for around a decade, I’m well aware of how interfaces work. Yet, my experience is that breaking changes in a language like Java happen far more frequently than they do in a language like Clojure. So, while there are mitigating factors in theory, I don’t find they translate to having stable APIs in practice. That’s just my experience though.

                                                                            2. 1

                                                                              How does you choosing what data you return from the API prevent you from exposing implementation details? Or are you saying you just don’t care because you can change the API interface whenever you feel like it?

                                                                              1. 1

                                                                                I don’t really follow your question. When I write a function, I explicitly what data it returns, and the shape of that data. The shape of the data is explicitly part of the API contract.

                                                                                1. 1

                                                                                  The shape of the data is explicitly part of the API contract.

                                                                                  Because the approach forces you to do that even when the shape of the data is an implementation detail that you don’t want to expose.

                                                                                  1. 1

                                                                                    That statement doesn’t make sense. The shape of the data is explicitly part of the contract provided by the API. It is not an implementation detail.

                                                                                    1. 1

                                                                                      Every implementation choice that is not relevant to the API’s core functionality is an implementation detail. For example, it’s not relevant for an iterator to know the data structure of a collection. All the iterator cares about is that it can iterate over the elements. The concrete shape of the collection (list, set, map, queue, stack, whatever) is an implementation detail from the point of view of the iterator.

                                                                                      Just because you decide to make an implementation detail a part of the API’s contract, that doesn’t mean it’s not an implementation detail anymore. That’s essentially the problem in the article that I linked.

                                                                                      1. 1

                                                                                        In Clojure, you have a seq interface, so any function can iterate over any collection. The same way as it works with Java interfaces. So, no you’re not leaking any more implementation details than you wold with OO API. You’re just arguing a straw man here.

                                                                                        1. 1

                                                                                          Just because iteration in Clojure does not depend on the implementation details of the data structure, that doesn’t mean the API’s clients can’t write code that does depend on such details. So, that does not contradict my point at all.

                                                                                          I don’t know Clojure that well, so I can’t give you an example in Clojure. However, I’m pretty confident that even Clojure does not magically solve the problem of leaking implementation details (otherwise, why would it have interfaces in the first place).

                                                                                          1. 1

                                                                                            Again, there’s no difference between FP and OO here. Both Clojure and Java have collection interfaces. If you have an OO API that says it returns a collection, that collection is backed by a concrete implementation, such as a list, internally. That’s an implementation detail. OO doesn’t magically do away with collections. And this is why I find your whole line of argument completely absurd.

                                                                                            Also, having actually used Clojure for a around a decade now professionally, I can tell you that what you’re describing has never ever come up in practice. So, forgive me if I’m not convinced by your arm chair analysis here.

                                                                                            1. 1

                                                                                              Again, there’s no difference between FP and OO here. If you have an OO API that says it returns a collection, and that collection is backed by a list internally, that’s an implementation detail. OO doesn’t magically do away with collections.

                                                                                              I don’t follow this argument at all. Yeah, in the OO case there is an implementation detail. However, as you pointed out with the word internally: that implementation detail is not exposed to the client. Meaning, the API owner can change that list to a map or a tree or any other data structure at any time without having to worry about the client’s code at all.

                                                                                              It’s also a little bit of a straw man because this discussion was not about OO vs FP. It was about your statement that APIs should produce/consume raw data rather than objects.

                                                                                              Having actually used Clojure for a around a decade now professionally, I can tell you that what you’re describing has never ever come up in practice. So, forgive me if I’m not convinced by your arm chair analysis here.

                                                                                              So, you’re telling me that, in a whole decade, you’ve never had to make changes to an API because a data structure turned out to be wrong? Given that I could pretty easily find an article that showed people having exactly that problem, forgive me if I conclude that either you’re not remembering correctly, or you’re an extraordinary programmer.

                                                                                              1. 1

                                                                                                The implementation detail is exposed to the client in exactly the same way. In both cases you have an interface that abstracts the type of collection used from the client. The API owner can change the implementation in EXACTLY the same way. If my API returned a list and then I change it to return a vector, the client does not need to make any changes because both of them support the seq interface.

                                                                                                The discussion is about OO vs FP because my whole point is that in OO you end up interacting with object graphs in your API, while in FP you end up interacting with plain data. I’m simply telling you that your argument is incorrect in the context of Clojure because collections conform to interfaces. I’m starting to get the feeling that you don’t understand how interfaces work.

                                                                                                So, you’re telling me that, in a whole decade, you’ve never had to make changes to an API because a data structure turned out to be wrong?

                                                                                                Correct, I’ve never had to make a change to an API because I changed the implementation of the backing data structure that conformed to the same interface.

                                                                                                And please do link me an article of this happening in Clojure since you claim you claim that can easily find that. Perhaps what you failed to consider is that the article you found deals with the problems in a specific language as opposed to a general FP problem that you extrapolated from it.

                                                                                                It frankly amazes me that somebody who openly admits to knowing nothing about the subject can have such strong opinions on it based on an article they find.

                                                                                                1. 1

                                                                                                  The implementation detail is exposed to the client in exactly the same way. In both cases you have an interface that abstracts the type of collection used from the client. The API owner can change the implementation in EXACTLY the same way. If my API returned a list and then I change it to return a vector, the client does not need to make any changes because both of them support the seq interface.

                                                                                                  As I said, only because Clojure happens to have a seq interface. This reply tells me that you didn’t really get the point I made, and I don’t know how to explain it to you at this point. It seems you just don’t want to see it.

                                                                                                  Correct, I’ve never had to make a change to an API because I changed the implementation of the backing data structure that conformed to the same interface.

                                                                                                  Then your API is not producing/consuming raw data, it’s producing/consuming interfaces. It seems you’re contradicting your own position.

                                                                                                  And please do link me an article of this happening in Clojure since you claim you claim that can easily find that. Perhaps what you failed to consider is that the article you found deals with the problems in a specific language as opposed to a general FP problem that you extrapolated from it.

                                                                                                  So, I searched “Clojure information hiding”, and the first hit literally repeats my whole point. Maybe you understand this explanation then:

                                                                                                  One of the goals of information-hiding is to hide implementation details, so that programmers cannot write programs that depend on those details. (If they do so and those implementation details change, then those programs must also be changed, driving up software maintenance costs.) Clojure thus does not give us any good way to hide implementation details.

                                                                                                  https://cs.calvin.edu/courses/cs/214/adams/labs/08/clojure/

                                                                                                  1. 1

                                                                                                    Clojure having a seq interface clearly disproves your point. I’m not sure what else there is to say here.

                                                                                                    Then your API is not producing/consuming raw data, it’s producing/consuming interfaces. It seems you’re contradicting your own position.

                                                                                                    If you think that then you didn’t understand my position. My original point was that data is immutable, transparent, and it doesn’t have behaviors associated with it. Having interfaces doesn’t invalidate any of that. Your whole argument is just a straw man.

                                                                                                    So, I searched “Clojure information hiding”, and the first hit literally repeats my whole point. Maybe you understand this explanation then:

                                                                                                    There is no information hiding in Clojure because data is part of the API. That’s my whole point of the difference between OO and FP. The claim that this is at odds with hiding implementation details is incorrect however, and I’ve already explained repeatedly why it’s incorrect.

                                                                                                    Seeing how this discussion just keeps going in circles I’m going to leave it here. You can feel free to continue believing what it is that you want to believe, I’ve explained all I can.

                                                                                                    Have a good day.

                                                                                                    1. 1

                                                                                                      My original point was that data is immutable, transparent, and it doesn’t have behaviors associated with it. Having interfaces doesn’t invalidate any of that.

                                                                                                      Okay, that changes things…

                                                                                                      I guess I didn’t understand that at all from reading your comments. There is a paragraph in your initial post where you talk about “classes being ad hoc DSLs” and a bunch of other stuff that applies equally to interfaces. You know, in C++ you make interfaces as pure virtual classes. Then, in the next paragraph you make a contrast between that situation and just data. So, I thought you were talking about concrete data structures not hidden behind any interface.

                                                                                                      Also, in your first counterargument, you’re countering with this graph of objects that’s hard to change. However, if that was a graph of interfaces, that problem would still be there (as you pointed out in another comment). So, this reinforced the understanding in me (and also others, it seems) that you were talking about concrete “interface-less” data structures.

                                                                                                      Anyway, if what you’re arguing for includes the ability to return only interfaces from APIs, then I agree the problem that I brought up doesn’t really apply.

                                                                                                      1. 1

                                                                                                        The problem with graphs of objects is primarily with objects being opaque and having behaviors. This is completely tangential to the discussion around interfaces.

                                                                                                        When you have a data driven API backed by immutable data, then what you see is what you get. You can see the the data returned in the API, and it doesn’t have any behaviors associated with it. All the interfaces achieve here is abstracting over the concrete implementation, so you don’t have to worry about the specific backing data structure affecting the API semantics.

                                                                                                        1. 1

                                                                                                          Alright, that’s great! I actually share the view that state can turn into a nasty thing. It’s just that you seemed to be arguing against the idea of data abstraction. This kind of triggered me because my experience tells me the code I work with professionally would never survive without it.

                                                                                                          I guess it’s words like transparent and opaque that cause some confusion for me. They are very generic words so they can be misinterpreted easily. For example, an interface is also opaque in the sense that you can’t see the implementation details.

                                                                                                          1. 2

                                                                                                            Glad we’re on the same page. Unfortunately, this kind of thing happens quite often. We all use the same words, but we attach subtly different meanings to them in our heads based on our existing knowledge and experience. So, it’s really easy to end up talking past each other and not even realize it. :)

                                                                          2. 2

                                                                            I basically agree with everything you said about what makes a program good or bad, but I disagree with your conclusion: that functional programming leads to good programs and oop leads to bad programs (in some general sense, let’s not nit-pick or talk about absolutes).

                                                                            But I disagree. In almost all languages, you are perfectly allowed to mutate inputs into functions. This includes basically all (quasi-popular) functional languages that are not named Haskell or Clojure. You are also allowed to cause side effects in your functions in all functional languages that are not named Haskell. This means that I can write input-mutating, side-effecting functional code in OCaml, LISP, Scheme, Rust, etc, etc. Some languages discourage it more than others.

                                                                            My point is that I agree that making 100 opaque state machines to interact with is a bad idea for a program. But that ad-hoc, crappy, DSL is also perfectly easy to write in a functional way.

                                                                            I have little doubt that a very strict OOP language that does not allow input arg mutation and side-effects in methods is possible to create and would probably work just as well as good functional languages. The only difference would be a coupling of the data to the functions. That is the only actual difference between FP and OOP in any strict sense, IMO.

                                                                            1. 5

                                                                              Both major ML dialects (Standard ML and OCaml) keep a typed distinction between immutable and mutable data. I find this to be good enough to tell which operations can mutate data that I care about at any given moment. Moreover, modules allow you to easily implement ostensibly immutable abstract data types that internally use mutation. (The operating word here is “easily”. It is possible in Haskell too, but it is a lot more painful.)

                                                                              I would not call Rust a “functional language”, but, for similar reasons, its ability to track what data can be safely mutated at any given moment is good enough to get most of the advantages of functional programming. And then, some of the advantages of non-functional programming.

                                                                          3. 3

                                                                            Hopefully on-topic: My experience in functional programming has led to heavy use of composition.

                                                                            One thing that’s always frustrated me about Python is that instance methods do not return “self” by default, but instead return “None”. I once hacked up a metaclass to make that happen, and suddenly Python felt much more functional! SmallTalk and some of the heavy OO languages do return “self” or “this” by default, I find that fits the Haskell part of my brain better.

                                                                            What’s the zen koan? Objects are a poor man’s closures, and closures are a poor man’s objects? In Haskell I use partial application to give me roughly what object instances give me. It’s neat, try it out!

                                                                            1. 4

                                                                              One thing that’s always frustrated me about Python is that instance methods do not return “self” by default, but instead return “None”.

                                                                              Yes! It makes it much harder to do simple (list|dict|generator) comprehensions when mutations return None.

                                                                              In Haskell I use partial application to give me roughly what object instances give me. It’s neat, try it out!

                                                                              I (ab)use functools.partial for this in python. Very helpful when you have a defined interface and you want to stick extra parameters in as well.

                                                                            2. 3

                                                                              Even principled object oriented code hides race conditions. If you connect two systems in an OO language, it may produce an incorrect result while separately systems would run just fine.

                                                                              Another problem is that partial evaluation for an OO language is not obvious. If you intend to write abstract code like people can do with FP it introduces structure that may need to be contracted away.

                                                                              1. 2

                                                                                You don’t get a guarantee from the language, but you can absolutely structure your OO code so composition is thread-safe, and unsafe combinations are obvious.

                                                                                When I say dependency inversion of I/O and state, I mean that they are all instantiated in main(). And all other code “modules” are also instantiated in main (as classes), and receive state and I/O as parameters.

                                                                                If you pass the same state to two different modules, then you have to be careful that they are not run concurrently.

                                                                                If they don’t accept the same state as parameters, then they are safe to run concurrently.

                                                                                There are zero mutable globals. That is how the Oil interpreter is written.

                                                                                It helps as I mentioned to have some classes that are like functions, and some classes that are like data. Data and functions are both usefully and easily expressed as classes. (In Oil I use ASDL to make data-like classes, for example)


                                                                                tl;dr You can make state and I/O parameters in an OO language, and then you get a lot of the reasoning benefits of functional programs, along with some other flexibility (like using a mutable style inside referentially transparent functions, mentioned in my comments and in the article)

                                                                                1. 1

                                                                                  Could you expand on your first point? What kind of systems and connection between them do you have in mind?

                                                                                  1. 3

                                                                                    An example, a simple one:

                                                                                    class A {
                                                                                      int x;
                                                                                      mutate_x (int v) { x = v };
                                                                                      sendto (B receiver) {
                                                                                        int y = x + 10;
                                                                                        while (x < y) { receiver.receive(x); x += 1 }
                                                                                      }
                                                                                    }
                                                                                    

                                                                                    Depending on whether a receiver here has a separate access to A and gets to mutate_x when sendto is going on, this code is either fine, or then it’s not.

                                                                                    1. 1

                                                                                      That makes sense. Thanks for elaborating!

                                                                                2. -1

                                                                                  Can you please paste you replies here, so I don’t have to make another click?

                                                                                1. 2

                                                                                  I wish that I understood monads in Haskell well enough to appreciate this article - but I’ve had so much trouble trying to grasp how monadic IO and state work under-the-hood that I’ve never gotten around to learning about the different ways that they can be used.

                                                                                  In particular I remember reading these paragraphs from two different articles:

                                                                                  From Unraveling the mystery of the IO monad

                                                                                  “When we teach beginners about Haskell, one of the things we handwave away is how the IO monad works. Yes, it’s a monad, and yes, it does IO, but it’s not something you can implement in Haskell itself, giving it a somewhat magical quality.”

                                                                                  And from Pure IO monad and Try Haskell

                                                                                  “As Haskellers worth their salt know, the IO monad is not special … I’d recommend Haskell intermediates (perhaps not newbies) to implement their own IO monad as a free monad, or as an mtl transformer, partly for the geeky fun of it, and partly for the insights.”

                                                                                  These inconsistent descriptions of how much magic these monads involve is, for me, a far bigger source of confusion than notation.

                                                                                  1. 4

                                                                                    Here’s a short explanation of what’s “going” under the hood. It’s shortly a problem of representing interactions using functions.

                                                                                    You could think that under every “print”, etc. there’s a function that takes a world and gives a world where something’s been printed to your screen. Now you can chain these functions to do input/output.

                                                                                    But how about you write a function that fills a type such as World → (World, World)? There’s a trick to preventing this. Lets say that you must return a function World → World, and the result you return determines which interaction is actually happening when you run the program, you can still create “speculated” images.

                                                                                    But there’s still a problem, how about you do this:

                                                                                    let (x, w1) = read (print "input?" world)
                                                                                    in print ("hello " ++ x) world
                                                                                    

                                                                                    To make it clear, this program is producing it’s output from results that cannot be produced because they are result of speculation.

                                                                                    It’s possible to limit production of results such that you can only read those results that you actually chained to being part of the interaction. This is achieved by retrieving the result in a function. Eg. you get the result by passing in: input → World. Now the system can prepend the interaction to be part of the result and you no longer can use up values that end up being pure speculation.

                                                                                    “Under the hood” the value you returned is deconstructed and steps are taken to produce the interaction that’s represented.

                                                                                    If you need more details, I can provide, but eventually they become implementation dependent. The first article you got is probably slightly wrong because it conflates things. You can roll your own IO monad inside Haskell.

                                                                                    {-# LANGUAGE GADTs #-}
                                                                                    
                                                                                    module MY_IO where
                                                                                    
                                                                                    data MyIo a where
                                                                                        M_print :: String -> MyIo ()
                                                                                        M_getLine :: MyIo String
                                                                                        M_bind :: MyIo a -> (a -> MyIo b) -> MyIo b
                                                                                    
                                                                                    m_hello :: MyIo ()
                                                                                    m_hello = M_getLine `M_bind` step2
                                                                                        where step2 name = M_print ("Hello " ++ name)
                                                                                    
                                                                                    m_interpret :: MyIo a -> IO a
                                                                                    m_interpret (M_print s) = print s
                                                                                    m_interpret (M_getLine) = getLine
                                                                                    m_interpret (M_bind x f) = m_interpret x >>= m_interpret . f
                                                                                    

                                                                                    You can also consider how it works if you replace “data MyIo” by “class MyIo”.

                                                                                    Think that it’s not real IO monad because “it’s been implemented with IO”. Well.. You don’t need to implement it with IO.

                                                                                    l_interpret :: MyIo a -> [String]
                                                                                        -> Either [String] (a, [String], [String])
                                                                                    l_interpret (M_print s) x      = Right ((), x, [s])
                                                                                    l_interpret (M_getLine) (x:xs) = Right (x, xs, []) 
                                                                                    l_interpret (M_getLine) []     = Left []
                                                                                    l_interpret (M_bind x f) xs = case l_interpret x xs of
                                                                                        Left ys1 -> Left ys1
                                                                                        Right (z,xs1,ys1) -> case l_interpret (f z) xs1 of
                                                                                            Left ys2 -> Left (ys1 ++ ys2)
                                                                                            Right (q,xs2,ys2) -> Right (q, xs2, ys1 ++ ys2)
                                                                                    

                                                                                    For example, there it’s been interpreted as abstract interactions like that. And now you can examine the “m_hello” as potential interactions:

                                                                                    *MY_IO> l_interpret m_hello []
                                                                                    Left []
                                                                                    *MY_IO> l_interpret m_hello ["foo"]
                                                                                    Right ((),[],["Hello foo"])
                                                                                    *MY_IO> l_interpret m_hello ["foo", "bar"]
                                                                                    Right ((),["bar"],["Hello foo"])
                                                                                    

                                                                                    You can also treat it with continuations if you don’t like that the previous thing recomputes everything when you add an input.

                                                                                    1. 2

                                                                                      Most people tell me to just use them in Haskell in a variety of situations to understand them rather than worrying about what’s underneath. I still searched for a specific comment on internals that enlightened me quite a bit. I didn’t find it.

                                                                                      I’ll still share the search results since they had many interesting comments on the topic. One or more might be helpful. One even has an implementation of Maybe in the C language.

                                                                                    1. 1

                                                                                      To avoid the next OOPs. Say you have some command X. Ideally you’d like to predict what X does before you give it a go. This is precisely what type information (except in your language) does.

                                                                                      1. 3

                                                                                        It’s a hilarious post. It’s hard to recall a closed source vendor that hasn’t screwed over their users in some way. That people have been writing open source seem to be the reason why anything works at all.

                                                                                        Also I guess people don’t have problems with printers these days because they don’t own printers. Paperless office is a pretty good solution when it comes to CUPS.

                                                                                        Stuff on the bottom like the USB, Web, POSIX, suckets, ncurses, declare what kind of UX you’re going to have. It’s not the stuff on the surface.

                                                                                        I feel like Gruber still hasn’t figured it out and still thinks it can be fixed by papering “good UX” proprietary interfaces over stuff.

                                                                                        1. 6

                                                                                          I feel like Gruber still hasn’t figured it out and still thinks it can be fixed by papering “good UX” proprietary interfaces over stuff.

                                                                                          Considering he says exactly the opposite of this, I would disagree with your characterization.

                                                                                          1. 3

                                                                                            Stuff on the bottom like the USB, Web, POSIX, suckets, ncurses, declare what kind of UX you’re going to have. It’s not the stuff on the surface.

                                                                                            Which is exactly why Gruber argues that ESR’s ‘just make a good UI for it’ argument is wrong. UX needs to be considered from the very start of the application’s design, and a lot of Linux desktop software seems to lack cohesion. The number of times that I’ve seen a ‘fooProgram: bar failed’ error message from my DE is disappointing.

                                                                                          1. 6

                                                                                            I really don’t like the approach of using python itself as the configuration language. Stuff configuration into effectively global variables muddies dependencies and interfaces, and additionally it’s awkward as soon as a non-python component needs to read this configuration. I’ve also seen cases where the “config” module slowly starts acquiring things that perform side-effects, which is its own whole can of worms.

                                                                                            1. 1

                                                                                              Vim is configured with a similar script. If you want to do it this way probably depends on what you’re doing.

                                                                                            1. 5

                                                                                              I don’t see the “innovativeness” of this method. Solving by factorization is commonly taught in Algebra 1 (at least in US schools) as an alternative to the quadratic equation (see Khan academy for a refresher).

                                                                                              The problem with solving by factoring is that it doesn’t really work well when the x^2 coefficient is not 1 or when the values are fractions or irrational numbers because factorizing this way requires to find a pair of numbers that sums and multiplies to a set of two given numbers. In this case, you will need to use the quadratic equation.

                                                                                              If you are interested in a cool way to make factoring a quadratic equation simpler, CPM covers a good visual way to organize that work.

                                                                                              1. 1

                                                                                                The paper shows a simple way to get the quadratic formula from remembering few simple rules you should remember anyway. I dunno if there’s innovation there but it’s nice and removes the mechanical rote-memorization that rots school mathematics. That CPM link btw. is a mess.