1. 31

Great description of why it’s important to discourage and avoid cleverness.

  1.  

  2. 16

    TL;DR: If you have teams of >50 people and no code review, you are going to have a bad time.

    1. 1

      Did the article say they have no code review? It looks possible to run into this problem even with that in place.

      1. 1

        The footnote says:

        Code Reviews would have solved this issue as someone else would have stumbled on that symbol and asked for more clarity and understanding. That would have help introduced the library to the team easier than a surprise. Unfortunately, at Gravity we didn’t have required code reviews in place. We do have code reviews in place at CrowdStrike.

    2. 6

      Whats going on with that code sample? I’ve never written scala before, but that seems a little intense, I only have a little haskell experience to back me up.

      I love functionnal programming, but the lack of people who actually know how the more advanced bits work must surely be a big obstacle on its way to more mainstream use. Though I’ve yet to see a haskell sample I had more difficulty to grok than that scala example.

      1. 11

        Scalaz can really complicate code and some Scala teams discourage or prohibit its use. I admit that I don’t have a lot of experience with it, but perceive it is powerful and useful, albeit a bit arcane, and believe that it increases the learning curve of Scala greatly when used.

        Scala permits the use of type parameters, kinda like Java generics. In the article example, Foo[F[+_] : Monad, A, B] means that a Foo has three types that it cares about that are defined at compile/runtime: F, which holds anything (that’s _) and is a Monad, and A and B.

        When a Foo is instantiated, its constructor takes a few parameters:

        • an execute parameter takes a function that takes a Foo.Request[B], a Foo.Request that cares about B, and produces an F[B], or an F that cares about B
        • a joins parameters takes a function that takes a Foo.Request[A], a Foo.Request that cares about A, and produces another function that takes a B and produces a list of Foo.Request[A] objects.
        • lastly, an implicit parameter J that is a Foo.Join[A,B], or a Foo.Join that cares about A and B.

        A and B are consistent and the same class throughout the entire lifetime of the particular Foo object.

        That’s all the farther I feel like typing out at 0222 hours ;-)

        Some Scala people really like arrow notation and all kinds of single letters for type notation, in order to convey that they are type parameters and not actual types. I consider myself perhaps among a small number of heretics that favor readable code, and use full words for type parameters, commonly In, Out, and SomethingElse, if a third, middle state is required.

        For instance, a piece of a program I’ve been working on looks something like this:

        trait Chainable[In, Out] {
          def andThen[SomethingElse](next: Chainable[Out, SomethingElse]): Chainable[In, SomethingElse]
          def apply(thing: In): Out
        }
        

        If val barifyFoo: Chainable[Foo, Bar] and val bazifyBar: Chainable[Bar, Baz], then I can do val bazifyFoo = barifyFoo andThen bazifyBar and call val bazified = bazifyFoo(foo) with the same eloquence as something like val bazified = bazifyBar(barifyFoo(foo)) and let the implementation of andThen perhaps work out any shortcuts or concurrency.

        This might otherwise be written as trait Chainable[A, B] { def andThen[C](next: Chainable[B, C]): Chainable[A, C] def apply(thing: A): B } Which of the above is more readable for this simple trait building a chain of associative operations? Which is more easily explained to a wider range of developers, many of whom are unfamiliar with such type… magic? I’ll tell you through two years of experience teaching Scala that being explicit yields much more readable code at the expense of a few more keystrokes.

        1. 4

          To add to @colindean’s explanation:

          The implicit Foo.Join[A, B] is the idiomatic way to constrain the relationship between the types A and B, or that we know how to perform some kind of operation that’s supported for some pairs of types but not others. This can get complex, but offers power that you don’t get in Haskell. A better alternative is probably a full dependently typed language with native support for type functions e.g. Idris.

          ({type l[+a]=WriterT[F, Log[A, B], a]})#l is a pattern for expressing a partially applied type (unfortunately scala doesn’t have type-level currying, unlike Haskell). There’s a compiler plugin that would let you write it more clearly as WriterT[F, Log[A, B], ?]. TraceW is a similar type alias. The type itself is a standard monad transformer, WriterT: it’s saying we’re going to work with (functions to) pairs of a log and a value inside some context F[(Log[A, B], ?)], and we want to compose them by composing F in its native fashion and combining the logs in their native fashion. There is certainly something to learn here, but it’s an extremely general, reusable pattern, compared to e.g. annotation/AOP-based logging/tracing generators that are (IME) often equally complicated to understand, but much less general.

          The name :++>> is indefensible, but looking at the function what it does is very simple: it takes a function from a value to a log entry and adds that entry to the log, keeping the value the same. So execute returns a log containing just List(request -> request.response(repr, self.joins(request)(repr))).

          Looking at it this does seem unnecessarily complicated. I think it could be defined as just:

          def execute(request: Request[A]): WriterT[F, Log[A, B], B] =
            WriterT(self.execute(request) map {repr => (List(request -> request.response(repr, self.joins(request)(repr)), repr)})
          

          If I knew what joins did (it’s not in the given code) it might be possible to simplify further. We could expand the one-liner (e.g. name the log value rather than immediately putting it in a pair with repr) if that would clarify, though IMO people underestimate the cost of verbosity and it’s easy enough to mentally unpack these things.

          1. 8

            I don’t think this article is about Go. The point here is the Scala language (and a popular library) made it possible for the code to be opaque to his own teammates (who were also Scala programmers!).

            Choosing to use Go going forward is one possible conclusion. If the article said that they chose to just use vanilla Java 8 instead, I think the takeaway about the dangers of clever code would be the same.

            I remember when I first saw the potential issues of scaling Scala at Gravity back in 2009/10ish. It was close to the end of the day when we had a major issue reported in production that was affecting our large customers. Several of us started investigating and were able to track the source of the issue. The only problem was we had no idea what the code was doing at first. We came across a strange symbol we hadn’t seen in our projects before. The spaceship operator |> . Someone said out loud “what the hell is that?”. There was some other implicit magic going on that wasn’t immediately apparent. A CMD+B to traverse into the method yielded nothing in our IDE as it couldn’t find the symbol (IDE’s have improved here since). A quick googling of “|>” yielded nothing as well. We were stumped and didn’t have sources pulled down.

            The developer who wrote the code was unreachable on vacation so we had to figure it out. We noticed a new library was being included called scalaz, hours later we had tracked down the mystery symbol and grok’d what was going on, made the fix and life was good again. That blip turned a fix that should have taken minutes into a fix that took hours. That was the point I started seeing the split in our engineering team.

            As an aside, it reminds me of how many Java programmers have trouble with Guice because it makes it very difficult to understand how code actually fits together. Yet another dependency injection framework (Dagger) tries to fix this by generating codepaths you can manually inspect.

          2. 4

            Interesting argument about scaling, but why not Java then?

            1. 1

              I think he mentions this with the difficulties with scala, but that there is more to learn with the jvm and how to tune it than with the golang runtime (which is fairly simple aside from GOMAXPROCS)

              1. 7

                It’s fairly simple because Go lacks 99% of the interesting knobs for doing anything interesting.

                1. 4

                  That sounds like a feature.

                  1. 0

                    The feature of never having to deal with Go in mission-critical applications?

                2. 3

                  Since 1.5 GOMAXPROCS is no longer an issue, as it defaults to the number of cores.

                  1. 1

                    As of 1.6 beta it is still a variable you can configure. Not all production installs are on 1.5+ yet. It is worth mentioning as being a parameter you can tune.

                  2. 3

                    “More tuneable” should never be a downside. If the JVM required more tuning to match the performance of the go runtime that would be a fair criticism, but I would expect the JVM in default configuration to outperform go, and then you have the option of further tuning the JVM if the cost-benefit on doing so is positive in your circumstances.

                    1. 1

                      go outperforms tuned JVM in some benchmarks https://www.techempower.com/benchmarks/#section=data-r11&hw=peak&test=db

                      the simplicity of the runtime is a feature and allows for high performance because the runtime has to do less.

                3. 4

                  It’s worth saying that even scalaz has pulled back from the worst excesses of |*| style as of version 7. Scalaz will never be well documented due to hilarious open source politics and a founder who’s actively hostile to beginners, but I would expect/hope the community to migrate to Cats reasonably soon, which offers the same functionality with a much stronger focus on user-friendliness. Modern scala IDEs will handle such operators correctly in any case, as the article acknowledges.

                  There are still too many symbolic methods in many scala libraries, something that I wish would change (and make every effort to avoid in my own code). In the meantime code reviews are vital for anyone doing scala development. I love the language and I think the power is worth the costs, but it has does have its pitfalls and you do need process around them.

                  1. 4

                    Go sucks, but I think his points about Scala are valid. Java-in-Scala is terrible and Haskell-in-Scala is terrible. Java-in-Scala programmers unwittingly circumvent the type system (e.g. passing null instead of None, meaning that you have to check for it anyway, and Haskell-in-Scala programmers end up using patterns that make sense in Haskell but become inelegant in Scala.

                    Implicits are also painful to debug, SBT is a nightmare, and the language feels like Magic: the Gathering where if you leave for a year and come back to it, there are all these new rules that have changed the game entirely. Then there are the slow build times.

                    I’m going to avoid saying too much about IDEs. If IDEs are seen as necessary rather than as an occasional nice-to-have, then you need to move upstream in your hiring. Sure, I’d probably use an IDE if I had to contend with a massive Java codebase, but they’re not integral to how I think about programming. Whenever people say they’re resistant to a new language because it lacks IDE support, I want to fucking scream.

                    All of that said, I don’t know that Go will solve his problems. Sure, it will prevent the chaos of overly clever Scala code, but everything Go lacks will be remade in multiple ways by different developers, Greenspun-style, and instead of contending with 2 different schools of design/development, there’ll likely be many more than just two in-house cultures around how to work around Go’s limitations.

                    1. 2

                      Whenever people say they’re resistant to a new language because it lacks IDE support, I want to fucking scream.

                      Why so? I dont mind hacking C++ and scheme on emacs for leisure, but when you’re picking the tools to create a project, people want (need?) to be productive, and proper tools (read IDEs, usable debuggers, autocomplete, …) is a must-have, not a nice-to-have, for that. I don’t get the hate towards people who want proper tools :)

                      1. 1

                        Implicits are also painful to debug, SBT is a nightmare, and the language feels like Magic: the Gathering where if you leave for a year and come back to it, there are all these new rules that have changed the game entirely. Then there are the slow build times.

                        The language has slowed down a lot in response to this kind of criticism. Five years ago was very different from three years ago, but I’d say it’s very much the same language it was two years ago.

                        SBT is awful. I don’t know why people use it. Just ignore it and use maven.

                        Implicit debugging is relatively well-supported in IntelliJ these days, or with -Xlog-implicits on the command line.

                        Build times are better than they were, though that’s always relative.

                        1. 1

                          Implicit debugging is relatively well-supported in IntelliJ these days, or with -Xlog-implicits on the command line.

                          This is interesting to reflect on. A fairly common language feature requires an IDE or some compiler option in order to understand what is going on. Specifically: the code itself is difficult to understand without help.

                          I hope in a decade or two, features like this will be seen as a red mark on a language. IDE support, or not, being able to understand code just by reading it is an important feature, IMO.

                          1. 4

                            Heh. I see it as just the opposite: it’s the first effective language that’s starting to make use of the power of modern UIs, even if only at the level of highlights and underlines. Mouseover gives an extra gradation between “displayed” and “not displayed” that’s perfect for certain kinds of operations that you want to be mostly hidden but not completely invisible. I really think anyone who talks about graphical/visual programming and moving beyond the text editor would do well to look at a modern Scala IDE, and see how a restrained use of graphical elements can enhance the editing of program text rather than trying to replace text entirely.

                            1. 1

                              My comment was that a language feature requires IDE and special tooling to understand what the code is doing. Your response doesn’t make sense to me, though. One can build similar tooling for any language, is Scala better because it requires such tooling? In what way would we be worse off by making that information local such that tooling is not required to understand a program?

                              1. 2

                                In most languages you have to make a function explicit or completely invisible. E.g. I’ve worked on projects that used async in Python and you either made the yield points explicit or you had them sliced out invisibly. Either approach was clunky. With that particular example they solved it with language changes but that’s not a sustainable approach - you can’t do that for business domain cases.

                                I think it’s pretty common to have a business object that’s almost-but-not-quite another object - it’s maybe not 100% Liskov substitutable but it’s not different enough that business users think there even is a conversion process between them. (The specific example I’m thinking of is an underwriting stamp as a layer of a reinsurance contract, but the point is these are domain-specific things). We see a variety of approaches to these kind of problems - decorators in Python, annotations with AOP in Java, macros in a number of languages. IME all of these would be better off displayed as implicits (and we do see a little of that e.g. some IDEs have native support for Spring annotations, and will hide the annotation and highlight the field instead. Compare also IntelliJ’s “folding” of Java-pre-8 lambdas).

                                Making that information explicit in the text would make the program too verbose. While implicits can certainly be misused, they’re more often used for conversions that would be completely invisible in a text-only programming language.

                                1. 2

                                  I’ve never been bitten by code being explicit. I’ve been hurt by code being verbose, but that is different than explicit. For example, in Ocaml, making the import of a symbol as close to the usage of the symbol is easy and readable. One can even import a module just for a single expression.

                                  On the other hand, I have been bitten countless times by things happening implicitly. Some weird behaviour happens because I did something not realizing there was implicit behaviour that invalidated an assumption. Or spending a long time trying to determine why something was happening because of some implicit behaviour.

                                  So, while I can understand your perspective, it’s one I cannot relate to at all. We’re all a product of our experiences, I guess.

                                  1. 2

                                    If by implicit you mean not visible when working on the code with the normal tools for doing so then I agree. But I think Scala makes that space smaller, not larger, because the implicits let you make things visible that would make the code too verbose if you had to write them in text. The language culture (as I’ve experienced it) is all about using small wrapper types to track small distinctions, and lifting concerns like “audit event” or “needs to happen in a database transaction” or “performs I/O” to be handled by the type system, possibly implicitly. It’s wonderful to be able to pass a concern like that all the way up to the routing layer in Spray, and know that it can handle it (via the implicitly resolved marshaller for that type) because you’d get a compile-time error if it didn’t - whereas that’s at best a runtime error and sometimes not even that in every other web framework I’ve seen. And the only thing that makes that practical is that the marshaller doesn’t have to be explicit in the route definition.

                          2. 1

                            SBT is awful. I don’t know why people use it. Just ignore it and use maven.

                            Once I’m at “use maven”, I’m at “use another language”.

                            I had hoped that SBT would improve. I remember it being a recognized pain point as far back as 2011. Then again, almost no one likes their build tools.

                            1. 1

                              I think most people are pretty happy with SBT. All build tools suck, but SBT is one that sucks a lot less than others. You have to imagine that the bad parts of SBT are still magnitudes better than the good parts of the tools in Haskell, Go etc.

                              1. 1

                                almost no one likes their build tools.

                                Shrug. I love maven. I’ve even used it to build Python when I had to write that.