1. 7
  1.  

  2. 3

    This didn’t really go the direction I expected. I don’t really see how calling open directly is better and clearer. It would require a comment to really understand what it is doing, IMO. Whereas making a function like EnsureOpenWithPermissions or similar would be much clearer than the naked call to open.

    This post isn’t really about abstraction, it’s about an unwillingness to make one-off functions that make it clear what the intent of the author is.

    1. 2

      Agreed!

      This post isn’t really about abstraction, it’s about an unwillingness to make one-off functions that make it clear what the intent of the author is.

      Of course, a comment will also do this, but there is also no mention of it. Author could also use a call like access(2), which has the semantics of erroring if the permissions are not what you expect. Using the right functions for the task also helps make things clear.

      1. 1

        I’d find it clearer, as when I’m trying to understand the code I don’t have to jump around to find out what this function does and then keep that in mind along with the 10-20 other functions in a control flow.

        For example EnsureOpenWithPermissions would be a bad name for this function, as open(2) doesn’t set permissions for already existing files so I have to remember that this name is misleading. This sort of mistake is why you need to read all the code to see what it actually does (not just what the function name hints that it does), and why egregious abstractions can be frustrating to deal with. The direct call to open(2) is perfectly clear in what it does, as it’s using a standard well-known POSIX API.

        1. 2

          Whoops, I mean EnsureExistsWithPermissions or something like that, assuming the requirement is the file exists with a specific set of permissions (changing them if necessary) otherwise failure.

          For example EnsureOpenWithPermissions would be a bad name for this function …

          But you’re confusing the implementation with the desire. If the desire is to ensure a file exists with a specific set of permissions, that may or may not be implemented with open. With some decent names that actually match up to what they do, one does not need to read lots of code to understand what a function does, It’s as easy as “oh, this makes sure a file exists with a specific set of permissions”. With an expressive enough type system you can also encode what it means to fail into the type system, so one has to read even less code.

          The direct call to open(2) is perfectly clear in what it does, as it’s using a standard well-known POSIX API.

          It’s not clear to me. open(2) opens a file, why would someone open a file just to close it? What are they trying to express with that? One would, at least, need to comment the code to make the intent clear a well as what the error cases are (if the file already exists, is that an issue? If it exists but with the wrong permissions is that an error?) That’s a lot of context that an be addressed by putting it into a function with a sensible name.

          This blog post is arguing directly against what has been considered common sense for the last few decades: hide implementation behind APIs with sane semanitcs and names. If we should no longer be doing that, this blog post doesn’t give a strong reason why. IMO, I think this post and sentiment is a consequence of Go’s poor error handling. It’s impossible to sanely compose errors which incentives developers to flatten calls rather than wrap them.

          1. 1

            With some decent names that actually match up to what they do, one does not need to read lots of code to understand what a function does.

            Hard-won experience tells me to trust code, not names or comments. Especially when debugging.

            It’s as easy as “oh, this makes sure a file exists with a specific set of permissions”

            That’s all well and good in isolation. That function is part of the bigger structure though, and lots of little decisions like this can cascade into something that’s not so easy to handle.

            When trying to understand code you’ve never really seen before, you want to understand the core control flow to figure out what’s going wrong. Working through a pile small functions each with their own abstractions to discover each is actually 1-2 lines of simple code is not conducive to this goal. This is even worse when under pressure.

            With an expressive enough type system you can also encode what it means to fail into the type system, so one has to read even less code.

            Most problems I see are systems-level and business-logic failures, forgetting to catch a failure is rare. Badly handling a failure, not so rare - such as killing an entire request when you should have failed more gracefully. Type systems don’t help you if your spec is wrong. Real-world failures tend to be subtle, especially in the large code base of a distributed system worked on by hundreds of developers.

            I think this post and sentiment is a consequence of Go’s poor error handling.

            The error handling is incidental to the point and not brought up in the blog post.

            I’ve seen this example in the wild in several languages, the only thing here that’s specific to Go is the lack of default arguments and overloaded functions. That doesn’t really affect the core point, just this particular example in this particular language.

            1. 1

              Your response has a lot of counters to what I’ve said in it but no clear narrative. Could you state how you are suggesting one write code? For example, you are willing to call a function open but for the example in the post you prefer to call open rather than wrap the behaviour in a function such as EnsureFileExistsWithPermissions. Why? What is your criteria for deciding when to make a function and when not?

              When trying to understand code you’ve never really seen before, you want to understand the core control flow to figure out what’s going wrong.

              I don’t see who this becomes easier by not hiding implementation details in functions. How is it easier to read a bunch of naked calls to open rather than EnsureFileExistsWithPermissions? WIth the former, I have to do a lot of guessing at the authors intent or reading of comments. With the latter, I can see what the author intended and if my code is failing because the file does not exist or is permissions cannot be changed, I can read that function and understand what is going on. In fact, given your belief that understanding the core control flow is what one wants to do, how is what I am saying not self-evidenltly superior than the alternative? I am saying to have the core control flow as a series of function calls. How is this not easier to read, easier to write, and easier to debug?

              Type systems don’t help you if your spec is wrong

              I don’t really see what this comment has to do with the present discussion, but I’ll bite: Type systems also don’t hurt you if your spec is wrong. And when you discover your spec is wrong and try to fix it, a good type system aids significantly in refactoring the code to match it. Refactoring is the killer app of Ocaml’s and Haskell’s type system. I see no downside in terms of spec-correctness when it comes to a good type system. But your overall comment is missing what I am trying to say, IMO, which is that a good type system lets you encode more things at the type level which means you have to read the underlying code less. I have found this to be true in production environments with large code bases.

              1. 1

                What is your criteria for deciding when to make a function and when not?

                My rule of thumb is when there’s significant functionality that can reasonably be isolated and broken out, or when non-trivial code has been duplicated three times.

                How is it easier to read a bunch of naked calls to open rather than EnsureFileExistsWithPermissions? WIth the former, I have to do a lot of guessing at the authors intent or reading of comments. With the latter, I can see what the author intended and if my code is failing because the file does not exist or is permissions cannot be changed, I can read that function and understand what is going on.

                If you’ve got a problem in a large system, you first need to narrow down to the relevant subsystem and determine how the code actually works to verify you’ve found the problem. Naked calls make it easier to ascertain how the code really works, and what mitigations are open to you. For example if you’re seeing a sporadic failure that looks to be due to reading an empty file, one approach to find the problem is locate all the open() calls and work backwards as there’s likely a touch before the properly written atomic write code. Abstraction makes that harder.

                Later on when you’re fixing the code you cah to take a good look at intent, but you need to locate the problem first.

                Refactoring is the killer app of Ocaml’s and Haskell’s type system.

                I’ve not come across that argument before, the usual argument is about fewer bugs. The language with the strongest type system I’ve used in anger is Go, and refactoring there seems pretty good with what is a relatively simple type system. How much better do the more sophisticated type systems make it?

                I’ve seen refactoring work in many languages with varying type systems. Go has a simple type system and refactoring works well, Java has a fairly standard OO type system and plenty of tools, Python is more flexible and has tooling but things will break down if you do more complex things with classes. I’d wonder given how sophisticated Haskell’s type system is if it could become a barrier rather than a boon. My own use of Haskell hasn’t used anything too complicated, so I’m not sure how it’d work out on a massive codebase.

                1. 1

                  If you’ve got a problem in a large system, you first need to narrow down to the relevant subsystem and determine how the code actually works to verify you’ve found the problem. Naked calls make it easier to ascertain how the code really works, and what mitigations are open to you. For example if you’re seeing a sporadic failure that looks to be due to reading an empty file, one approach to find the problem is locate all the open() calls and work backwards as there’s likely a touch before the properly written atomic write code. Abstraction makes that harder.

                  Later on when you’re fixing the code you cah to take a good look at intent, but you need to locate the problem first.

                  A few things jump to mind in response to this.

                  1. Naked calls might make it easier to understand what the code is doing, but it makes it harder to understand what the author intended. I say “might” because having to read more code can often make it difficult to understand what the code is doing, that’s why we have functions. For example, answering the question of which peice of code is making the wrong assumptions is harder . Is the touch code wrong or the code that assumes the file exists
                  2. Working backwards, IME, is easier when calls are wrapped. For example, a large project might call open in thousands of places, most of which are useless. Wrapping it in a touch-like function means we can actually look backwards from there, cutting out a lot of opens that are useless. We can also use it like a binary search tool, and answer the question of “are all the usages that depend on touch correct? Good, I can remove those as possibilities”.
                  3. How can you fix code without knowing the intent? In the example given above, how do you determine if the touch code is wrong or the usage code is wrong without more data, either in the form of a comment or a function name?

                  In the end, I think you’d like code in the style I’m talking about and this comment section is just not adequate to explain it. I’ve used it extensively in several projects so far with great success. Debugging was really easy, it’s easy to identify X is wrong but code says it wants to do Y, therefore the problem is Z. I’ve used the style in a team with experienced devs and introduced it with junior devis. The junior devs noted how easy they found debugging production issues with this style.

                  In the end, I think this is often because most lines of code in a project do not have bugs. At best, naked calls optimize for the case where most lines of code have bugs, therefore one needs to see as much of the code as possible to debug something. But in the real case of most lines of codes not having bugs, the extra function calls provide clear intent to what the author wanted and ease of understanding the code flow.

                  I’ve not come across that argument before, the usual argument is about fewer bugs. ….

                  In a language with a powerful type system, one can encode a lot of information, easily, in the type system that makes doing really vicious refactors fairly pain free. To the point where once the refactoring compiles, one can feel fairly confident the code still works. A simple example of this that is used often in Ocaml code is taking values that would just be an int or string and wrapping them in a private type. I have a wrapper function for open that looks like: open : string -> mode -> flags -> file. The string is the file name, and mode and flags are int underneath, but the type system does not allow one to pass an int into those, they must first do something like mode_of_int. Under the hood, a mode and flag are just int, so there is no runtime cost, at all, but they are distinct types at compile time. So I can refactor open to be open : string -> flags -> mode -> file and all usages will now not compile, despite them actually being the same type underneath.

                  The above is a simple example, but very effective. There are more interesting examples, and parametric polymorphism helps makes expressing a lot of things even safer. @bitemyapp could probably give some more examples. The important thing to note is not that expressing these things are necessarily impossible in other languages (although doing it in Java is near-impossible, I’m not sure about Go), but that it is so simple to do without a runtime cost that there is no incentive to not write code this safe, refactor friendly, way

                  This post on Jane St’s website is a little anecdote about their experience:

                  https://blogs.janestreet.com/ocaml-the-ultimate-refactoring-tool/

                  1. 1

                    Working backwards, IME, is easier when calls are wrapped.

                    I’ve had the opposite experience. I’m often looking at one (massive) subdirectory of code, if it calls out to code in some other subdir to use a library that makes it harder to find all the places that the relevant call could be.

                    “are all the usages that depend on touch correct? Good, I can remove those as possibilities”.

                    I’ve certainly used that approach in the past, always good to prune.

                    In the example given above, how do you determine if the touch code is wrong or the usage code is wrong without more data, either in the form of a comment or a function name?

                    In this case it’s a common pattern that is being used incorrectly so it’s pretty obvious what’s going on - the presence of the touch is the problem and removing it will fix this part of it. Not all cases are this simple of course.

                    At best, naked calls optimize for the case where most lines of code have bugs,

                    My argument is not about bugs, it’s more about the capabilities of the human brain. I can only keep so much of a control flow in my head at once, and every small abstraction takes up some of that valuable space.

                    the extra function calls provide clear intent to what the author wanted and ease of understanding the code flow.

                    What I work with it typically more on the systems coding side of things. The intent is generally fairly obvious, it’s the plumbing and all the little details that trip you up.

                    For example an hour ago I was reviewing a simple PR to add support for user-specified http headers to Go program. The intent is clear, however the use of Add there may not be correct to allow for multiple values for a header - and the docs don’t say. I know how HTTP headers work from the spec, and that commas are significant for having multiple of the same header. So does the library do some form of sanitization or escaping so that we should be passing in a map of lists of strings rather than a map of strings? After digging through the code and several layers of abstraction, the answer is that it’s a straight copy and it handles multiple of the same header by giving one line each so this will all work as expected. (Of course the real problem here is that there’s two ways to do multiple of the same header in HTTP, but that’s the world we live in).

                    When debugging I try to check everything to this level of detail.

                    The important thing to note is not that expressing these things are necessarily impossible in other languages (although doing it in Java is near-impossible, I’m not sure about Go)

                    Go supports exactly what you describe, it works nicely though can be a bit annoying at times if a library isn’t well designed.

                    There are more interesting examples, and parametric polymorphism helps makes expressing a lot of things even safer.

                    That’s a simple example. I’m more interested in the complex ones that take full advantage of type theory.

                    1. 1

                      My argument is not about bugs, it’s more about the capabilities of the human brain. I can only keep so much of a control flow in my head at once, and every small abstraction takes up some of that valuable space.

                      But isn’t this the exact opposite? If I understand what you’re saying correctly, you’re saying we should have more naked calls, which increases the amount of information one needs to read. That seems like it would stretch what one can keep in their head with relatively irrelevant details, doesn’t it?

                      When debugging I try to check everything to this level of detail.

                      Yeah, I’m just not following how naked calls help this situation. But we’re all a product of our experiences.

                      Go supports exactly what you describe, it works nicely though can be a bit annoying at times if a library isn’t well designed.

                      I don’t really know Go, how does one accomplish this in Go?

                      That’s a simple example. I’m more interested in the complex ones that take full advantage of type theory.

                      Another tool I’ve used is phantom types, which allow one to have two distinct types with the exact same representation, similar to a private type, but it allows reusing operations on the different types in a contrainted way. An example of where I’ve used this is a Riak client:

                      https://github.com/orbitz/ocaml-riakc/blob/master/src/lib/conn.mli#L44

                      The idea being that get can return a value with multiple values in it (siblings) but put can only write a single value. These two types are represented underneath exactly the same but with the invariant that there are no siblings on a push forces that all put code paths go through an operation that ensure no siblings exist. In my own code, this has been useful for refactorings, I can move things around and be guaranteed by the type system that I do not do a put with siblings in it. Phantom types can be done in other languages, although they are a bit more cumbersome, in Ocaml and Haskell it’s really trivial to implement. On top of that, languages like Java (and I assume Go, but could be wrong) where all forms of polymorphism allow for sub-typing, it’s not possible to get the same guarantees.

                      Unfortunately I think I’ve reached the maximum in my ability to express what I think consitutes good code in this context without working with you and we haven’t been able persuade the other in favor. Thank you for the perspective and insight. Happy to continue the discussion on various typing tricks, though.

                      1. 1

                        That seems like it would stretch what one can keep in their head with relatively irrelevant details, doesn’t it?

                        It does only if the abstraction is something you’ve previously internalised, such as how I’ve internalised how open() works.

                        If I haven’t, it hinders me. For example I was recently debugging a database issue in a language I’m not too familiar with where I have a very strong suspicion as to what the problem is from dealing with identical symptoms in the past. The docs for the library in use don’t mention at all how the code works at the level I’m trying to work at, so I need to dig through it in detail to find if it contains the pattern I’m looking for (a connection pool in this case), if so how it works and then on to see if it has the flaw I suspect it has. If the the application wasn’t layered on three levels of libraries before it got to the actual database calls I’m familiar with, this would have been easier.

                        I personally find when I’m working with multiple layers of abstraction that all the post-conditions and potential flows are a bit difficult to track, especially where each layer tends to have no more than 4-5 lines of code I care about so you have to jump around a lot.

                        I don’t really know Go, how does one accomplish this in Go?

                        You typedef int to your new type, for example https://golang.org/pkg/time/#Duration This also works with more complex types. You are allowed to explicitly cast between types, but there’s no implicit casting.

                        Another tool I’ve used is phantom types, which allow one to have two distinct types with the exact same representation, similar to a private type, but it allows reusing operations on the different types in a contrainted way.

                        I can see the use for doing that at compile time, though I’m not at all familiar with Ocaml.

                        (and I assume Go, but could be wrong)

                        I think you’re correct. Go’s approach to OO is unusual, it goes for composition rather than inheritance so there’s no real subtyping. Polymorphism is possible with duck-typed Java-style interfaces.

                        1. 1

                          You typedef int to your new type

                          This is much weaker than the guarantee one gets from Ocaml, if I understand correctly. In Ocaml, you would do something like:

                          module Duration : sig
                              type t
                              val to_int : t -> int
                              val of_int : int -> t
                          end
                          

                          Which makes Duration.t a distinct type for int, you cannot cast between them but must ask nicely for the implementation to give you an int. Whether or not this distinction matters to someone I cannot say, to me I find it very powerful and useful. But it’s good that Go at least did not go with the C typedef semantics.

                          I think you’re correct. Go’s approach to OO is unusual, it goes for composition rather than inheritance so there’s no real subtyping. Polymorphism is possible with duck-typed Java-style interfaces.

                          Actually, Go’s solution is fairly well understood and it is subtyping, it’s just structual subtyping. Ocaml actually has the same thing, although it’s mostly unused.