1. 33

  2. 22

    The “O” part is consistently omitted by all comparisons. The most unusual thing about it is that the object system is structural rather than nominal, and there’s type inference for it. Classes are just a handy way to make many objects of the same type.

    For example, the type of a function let f x = x#foo () is < foo : unit -> 'a; .. > -> 'a, which means “any object that provides method foo with type unit -> anything:

    # let f x = x#foo () ;;
    val f : < foo : unit -> 'a; .. > -> 'a = <fun>
    # let o = object 
      method foo () = print_endline "foo" 
    end ;;
    val o : < foo : unit -> unit > = <obj>
    # f o ;;
    - : unit = ()
    # let o' = object
      method foo n = Printf.printf "%d\n" n 
    end ;;
    val o' : < foo : int -> unit > = <obj>
    # f o' ;;
    Line 1, characters 2-4:
    Error: This expression has type < foo : int -> unit >
           but an expression was expected of type < foo : unit -> 'a; .. >
           Types for method foo are incompatible
    1. 17

      I regularly recommend people to the OCaml object system. Not only is it decently nice (but underused because other parts of the language are more popular and cover most of what you might want) but it also challenges your ideas of what “OO” is in very credible and nice ways.

      Classes are clearly separated from object types and interfaces. Inheritance clearly relates to classes as opposed to objects. Structural typing, which exists throughout the OCaml universe, makes clear what subtyping is offering (e.g., a more restricted sort of subsumption than structural classes will give you).

      It’s just an alternate universe Object system which is entirely cogent, replicates most of the features you might expect, but does so with a sufficiently different spin that you can clearly see how the difference pieces interact. Highly recommended!

      1. 6

        OCaml’s object system reflects what type theorist sees in an object-oriented language. You know, the kind of people who write things like TaPL’s three chapters on objects. What these people see is static structure, which, from their point of view, is fundamentally flawed, e.g., “it is wrong to make either Circle or Ellipse a subtype of the other one” and “inheritance does not entail subtyping in the presence of binary methods”.

        However, what a type theorist sees does not always match what a programmer sees. A conversation between a type theorist and a programmer could go this way:

        • Type theorist: Here, look! I have finally devised a way to adequately describe object-oriented languages!
        • Programmer: By “adequately describe”, do you mean “give types to”?
        • Type theorist: Of course. Sound types describe the logical structure of programs, in particular, your object system.
        • Programmer: I do not see how or why you can make such a claim. The object systems that I use are dynamic and programmable, so that I can adapt the system to my needs, instead of getting stuck at a single point in the language design space. Are you saying that your type system changes the type of every object every time I reconfigure the system?
        • Type theorist: No. I just describe the parts that will not change, no matter how you reconfigure the system.
        • Programmer: Then you are not describing much that is useful, I am afraid.
        • Type theorist: By the way, I have concluded that your system has the grand total of exactly one type.
        • Programmer: sigh

        And another conversation could go this way:

        • Programmer: (to a newbie) The internal state of an object is hidden from the rest of the program. Objects communicate by sending messages to each other.
        • Newbie: What is the point to this?
        • Programmer: This helps you compartmentalize the complexity of large software. Each object manages a relatively small part of the program’s state. Small enough to understand.
        • Newbie: Ah, okay. I can see why you would want to work this way.
        • Type theorist: Objection! You claim the state of an object is encapsulated, but your development tools are fundamentally based on the ability to inspect the internal state of every object, even when it is running.
        • Programmer: Right. In an object-oriented system, encapsulation is not enforced by the language, but is rather a property of how the system is designed.
        • Type theorist: So, in other words, encapsulation is a property of how you program the system, rather than of the system itself.
        • Programmer: That is not quite precise. Encapsulation is a property of the system. However, the system is more than just the language it is programmed in.
        1. 8

          So are you saying that the only real-world usable object systems are dynamically-typed ones? One of the most respected OOP teachers and language designers (Bertrand Meyer, of Eiffel and Design by Contract fame), disagrees with you on that one: https://en.wikipedia.org/wiki/Eiffel_(programming_language)

          1. 3

            In retrospect, my comment was much harsher than I wanted it to be. What I really want to see is a CLOS-like object system, embedded in a static language, preferably ML.

            In practice, the most popular object systems are not fully static. Java and .NET rely on runtime class metadata for lots of things. And, even if they didn’t, I doubt their designers would trust their own designs to be safe enough to remove all the runtime type checking.

            Eiffel would be a much better argument for your position if it were actually type safe. Pony is, to the best of my knowledge, the most widely used fully type safe object-oriented language.

            1. 2

              Hmm, Ada certainly takes a lot of inspiration from Eiffel. And I’d say Ada is probably more popular than Pony. Actually I’m not even sure if Pony is ‘OOP’ in the sense that the other mainstream languages are. AFAIK it is actor-based.

              1. 2

                Oops, you are right there.

                1. 2

                  At least according to Wikipedia, Ada precedes Eiffel in time.

                  Certainly one of the few things I know about Ada is that it supports programming by contract.

                  1. 3

                    But I think Ada’s contract system is inspired by Eiffel’s though?

                    1. 5

                      Correct, Eiffel is where design-by-contract originated. Ada added support in the 2012 spec.

                      See the comparison here, where it refers to “preconditions and postconditions” (since “design-by-contract” is now a trademark owned by Eiffel Software.

                      1. 1

                        Thanks for clearing this up for me!

            2. 4

              I feel like you’re arguing a lot for dynamic types as opposed to static types. That’s totally fine. I’m speaking pretty much entirely to the static components and trying to talk about something pretty different.

              There’s a different discussion, not really related at all to OO, about how the things you’re discussing here can be interpreted via types. I’m personally of the belief that most programmatic reasoning could be modeled statically, though perhaps not in any system popularly available. To that end, speaking using types can still be important.

              Which, to the point here, we can model a lot of these dynamic interactions using types at least temporarily. And there remains a big difference between the way that “factories for creating objects” can inherit from one another and the way that “one object serves as a suitable stand-in for another” is a different, if sometimes related, concept.

              1. 1

                I feel like you’re arguing a lot for dynamic types as opposed to static types.

                More precisely, I’m arguing for the use of dynamism in object systems, even (or perhaps especially!) if they are embedded in languages with static types. Multiple static types are already useful, but they would be even more useful if the dynamic language’s unitype were one of them, in a practical way.

                Not everything needs to be object-oriented or dynamically typed, though. For example, basic data structures and algorithms are best done in the modular style that ML encourages.

                1. 2

                  That’s a reasonable thing to ask for. I think many statically typed languages do offer dynamic types as well (in Haskell, for instance, there’s Dynamic which is exactly the unittype you’re looking for). Unfortunately, whenever these systems exist they tend to be relegated to “tricking the type system into doing what you want” more than playing a lead role.

                  Personally, this makes sense to me as when 80% of my work is statically typed, I tend to want to extend the benefits I receive there more than introduce new benefits from dynamicism.

                  I’d be interested to see if there were some good examples of how to encode an interesting dynamically typed idiom into something like Haskell’s Dynamic.

                  1. 1

                    Haskell’s Dynamic would not work very well in ML. Usually, the runtime system can only determine the concrete type of a given value. But the same concrete type could be the underlying representation of several different abstract types! Haskell gets away with having Dynamic, because it has newtype instead of real abstract data types.

                    The unitype embedded in a multityped language should only be inhabited by dynamic objects (whose methods are queried at runtime), not by ordinary values that we expect to be mathematically well-behaved, such as integers, lists and strings.

                    1. 2

                      I may not fully understand, but that feels doable in both ML and Haskell.

                      In ML, when we work dynamically we’ll be forgetting about all of the abstract types. Those have to be recovered behaviorally through inquiring the underlying object/module/what-have-you.

                      In Haskell, we can have something like newtype Object = Object (Map String Dynamic) possibly with a little more decoration if we’d like to statically distinguish fields and methods. You could consider something like newtype Object = Object { fields :: Map String Dynamic, methods :: Map String (Object -> Dynamic) } for a slightly more demanding object representation.

                      1. 1

                        In ML, when we work dynamically we’ll be forgetting about all of the abstract types. Those have to be recovered behaviorally through inquiring the underlying object/module/what-have-you.

                        This is precisely what I don’t want. Why do I need to give up my existing abstract data types to work dynamically? I have worked very hard to prove that certain bad things cannot happen when you use my abstract data types. These theorems suddenly no longer hold if the universe of dynamic values encompasses the internal representations of these abstract data types.

                        Instead, what I’m saying is: Have a separate universe of dynamic objects, and only then embed it into the typed language as a single type. If you don’t mind the syntactic inconvenience (I do!), this can already be done to some extent, in an ugly way, in Standard ML (but not Haskell or OCaml), using the fact that exception declarations are generative. But this is a hack, and exceptions were never meant to be used this way.

                  2. 1

                    C# 4.0 introduced dynamic type, and it pretty much works that way.

                    1. 2

                      The whole C# language is dynamic. It has downcasts, the as operator, runtime reflection for arbitrary types, etc.

                      What I want is a language that has both a static part, with ML-like “guaranteed unbreakable” abstractions (so no reflection for them!), and a dynamic part, which is as flexible and reconfigurable as CLOS.

                      1. 1

                        Having a function or operator that consumes an “obj” doesn’t make an entire language “dynamic”, by this definition literally every language is ‘dynamic’. I don’t love C# but its important to keep the discussion honest.

                        If you’re looking for one that does dynamics “as good as CLOS” while also doing static types you won’t ever be happy. It’s like saying I want something that is completely rigid, but also extremely flexible. If you have access to both extremes in one environment, your types will become a dead weight and your flexibility will be useless. If you permit a compromise then you can get what you want, but rightly you don’t want a compromise.

                        If you start with the solution instead of describing your actual needs you’re not going to find what you desire. What I found is that I value being able to express a wide variety of constructs statically, and with a degree of flexibility in my consumption of types. We can get close to how that might feel in F# through sum types, bidirectional type inference, operator overloading, and statically resolved type parameters. This may or not fit your needs, but it made me happy.

                        1. 1

                          Having a function or operator that consumes an “obj” doesn’t make an entire language “dynamic”, by this definition literally every language is ‘dynamic’.

                          I can guarantee you, Standard ML is not.

                          If you’re looking for one that does dynamics “as good as CLOS” while also doing static types you won’t ever be happy. It’s like saying I want something that is completely rigid, but also extremely flexible.

                          This is a mischaracterization of what I said. Re-read the technical specifics: the dynamic universe should start completely separate from the static one, and only then should we inject the former into the latter as a single type. In other words, the dynamic universe does not need to include literally every value in the static universe. It only needs to contain object references. A value whose static type is dynamic could be a file, a window, a game entity, but not an integer, a string or a list - not even a list of dynamic objects!

                          If you start with the solution instead of describing your actual needs you’re not going to find what you desire.

                          I think I stated my needs in a pretty clear way. I want (0) static stuff to be static, (1) dynamic stuff to be dynamic, (2) static stuff not to compromise the flexibility of dynamic stuff, (3) dynamic stuff not to compromise the proven guarantees of static stuff. The way C# injects literally everything into the dynamic universe compromises the safety of static abstractions. Sadly, this is not possible to work around, except by hiding the entire .NET object system, which defeats the point of targeting .NET as a platform. Java suffers from similar issues.

                2. 2

                  I am just a programmer, and know (very) little of type theory. I’m sorry, but I didn’t really understand your point(s?). Could you explain it in a way that doesn’t assume I understand the perspective of both parties (or just ELI5 / ELI-only-know-JS)?

                  1. 7

                    In the first conversation, the type theorist begins by being proud that he came up with a sound static type system for an object-oriented language. (Basically, OCaml, whose object system is remarkable in that it doesn’t need any runtime safety checks.) Presumably, before this, all other object-oriented languages were statically unsafe, requiring either dynamic checks to restore safety, or simply leaving open the possibility of memory corruption.

                    The programmer’s reply is the same point that Kiczales et al make in the introduction of The Art of the Metaobject Protocol. A completely fixed object system (e.g., those of OCaml, Eiffel and C++) cannot possibly satisfy the needs of every problem domain. So he uses object systems that allow some reconfiguration by the programmer (e.g., CLOS), to adapt the object system to the problem domain, rather than the other way around. Hence, by design, these object systems offer few static guarantees beyond memory safety.

                    In the second conversation, the programmer talks about the benefits of encapsulating state in an object-oriented setting. The type theorist retorts that many object-oriented languages don’t really do any sort of encapsulation, because you can just up and inspect the internal state of any object anytime. (By contrast, in a modular language, like ML, you really can’t inspect the internal representation of an abstract data type. The type checker stops you if you try.) The programmer acknowledges that this is indeed how object-oriented languages work (at least the ones he uses), but then says that encapsulation is still a property of the systems he designs, even if it is not a property of the languages he uses to implement these systems.

                    The point of the second conversation (along with other points) is better made in this essay by Richard Gabriel about incommensurability in scientific and engineering research.

                    1. 1

                      Got it, thanks!

              2. 3

                How does OCaml’s structural record system compare with row types e.g. in PureScript or Elm or Ur? (Note: Structural (subtyping) is distinct from row types.) Does OCaml avoid the soundness issues of subtyping, e.g. writing an object of two fields into a mutable cell and then reading back an object of one field (subtype), thus accidentally losing a field (but which may or may not be printed if we print the object)? This is something that row types avoid.

                1. 3

                  What you describe doesn’t seem like a soundness issue. It seems like normal upcasting and virtual dispatch. Btw OCaml doesn’t have structural records, it has structural typing of objects (i.e. OOP) using a row type variable to indicate structural subtypes of an object type. E.g. here’s a general description of entity subtypes:

                  type 'a entity_subtype = < id : string; .. > as 'a

                  And here’s a specific entity subtype:

                  type person = < id : string; name : string > entity_subtype

                  The .. above is a row variable: https://v1.realworldocaml.org/v1/en/html/objects.html#idm181614624240

                  1. 3

                    Thanks. My best effort to parse your post yields the following observations:

                    type 'a entity_subtype = < id : string; .. > as 'a

                    is OCaml’s way to write the equivalent of

                    type EntitySubtype r = { id :: String | r }

                    in PureScript. The r is a type variable referring to an unspecified record. Then

                    type Person = EntitySubtype { name :: String }

                    which, by replacing the r with { name :: Sting }, yields { id :: String, name :: String }.

                    So, based on the documentation, OCaml has normal row types for its record system. It’s sometimes hard to parse OCaml discussion due to the “OO” syncretism.

                    1. 2

                      Right! PureScript record row types, and merging them together, has a really nice syntax. OCaml’s is comparatively more primitive and explicit. It causes a little frustration when modelling hierarchies of JavaScript types in the BuckleScript/ReasonML community. We usually recommend using modules and module types as much as possible though. They’re also super powerful, and target JavaScript classes quite well by using some BuckleScript binding constructs.

              3. 7

                There is no do-notation […], athough it looks like it’s about to change.

                The cited reference is from April. OCaml 4.08 (and 4.09) have been released since then, making it easy to provide custom let-like and and-like operators. You can see some examples of these being used in https://roscidus.com/blog/blog/2019/11/14/cicd-pipelines/


                let example2 commit =
                  (* Fetch the source code and Docker base image in parallel: *)
                  let* src = fetch commit
                  and* base = docker_pull "ocaml/opam2" in
                1. 4

                  Not to mention ppx_monadic and friends existed for a long time before that. I should rid my projects of those and switch to the new built-in syntax I suppose.

                2. 5

                  If Haskell is a niche language, then OCaml is a super-niche language

                  It has certain popularity in Javascript ecosystem because of Bucklescript, but mostly in the form of ReasonML. Not sure, however, if it’s just a (smallish) hype; and Bucklescript originated from the same fintech niche that uses “regular” Ocaml.

                  1. 6

                    I think it may be a little misleading to claim that OCaml is “more niche” than Haskell.

                    I think the userbase may be smaller but the industrial adoption appears to be higher. Facebook, Docker, Citrix are the big 3 who use OCaml throughout their systems. Most the Haskell use in industry that I have found by contrast tends to be one person wrote it into their small project. For Haskell, AT&T uses it for complaint management, NVDA uses it for some small tools. That’s not to say that Haskell isn’t used extensively in any business, but rather that of the major labels who do use haskell, it does not tend to be used for major projects. By contrast for OCaml the businesses who do use it, use it extensively.

                    1. 4

                      Well, Facebook Messenger is written in it. That project was sponsored by Facebook (which already had quite a few OCaml projects) rather than anything fintech AFAIR.

                      1. 1

                        I guess the reference is to Bloomberg’s involvement with Bucklescript.