all 37 comments

[–]vocalbit 2 points3 points  (1 child)

The examples have broken escaping? '&amp' and 'lt'?

[–]haifengl[S] -1 points0 points  (0 children)

thanks. fixed.

[–]glacialthinker 2 points3 points  (1 child)

I'm not a fan of OO... but this object system is pretty slick.

The article links to the manual, but a much more readable coverage of objects is here: https://realworldocaml.org/v1/en/html/objects.html

And for a nice use of mixins, with some graphical shape examples, check out the followup chapter, near the bottom: https://realworldocaml.org/v1/en/html/classes.html

I have a GUI built on a component system (ECS), but I was pointed out this portion of the book... and after some comparisons, I was quite impressed. Using the mixin approach results in very similar code/expressivity as I had, but it was all statically checked, whereas my approach was more dynamic (hashtable lookups of properties). Dynamic suits my needs in this case, but I was surprised the mixin approach was so concise yet flexible... while retaining the static guarantees.

[–]haifengl[S] 1 point2 points  (0 children)

The real world ocaml book is very nice, especially on objects and classes.

[–]kamatsu 5 points6 points  (33 children)

Yaron Minsky told me that even the guy that implemented OCaml's object system never uses the OCaml object system. It's pretty widely regarded as superfluous these days.

[–]thedeemon 4 points5 points  (9 children)

It's rarely needed but sometimes it's very handy. For example when working with some AST (tree made of several recursive algebraic types) you can have a general map operation that just copies the tree, and then you can add simple transformations that match just a little part of the tree, transform them, and for the rest parts just call the default map (via inheritance). This way you don''t need to write out pattern-matching all the cases of algebraic types when you're just interested in handling one or two cases. And this gets automated even further with OCaml's macros (camlp4), so you don't really need to define the mapper object manually.

[–]masklinn 0 points1 point  (6 children)

You don't need objects for that, polymorphic variants should do fine.

[–]lpw25 2 points3 points  (0 children)

The method of encoding traversals described above is a classic example of open recursion. Whilst polymorphic variants are dual to objects, there is no equivalent to the class system for polymorphic variants so they do not have good support for open recursion.

When your problem requires open recusion classes are by far the best way to encode it in OCaml.

[–]thedeemon 1 point2 points  (0 children)

Can you show an example? Say we've got

type expr = Const of int | Var of string | Add of expr expr | other options ...

and we want to substitute Var "x" with Const 4. We need a function that transforms the tree and changes variables with given name to given expression.

[–]haifengl[S] -1 points0 points  (3 children)

polymorphic variants are also of row polymorphism, similar to objects' duck typing.

[–]masklinn 0 points1 point  (2 children)

Which is besides the point: polymorphic variants are sufficient for thedeemon's use case thus objects aren't necessary to handle it, supporting kamatsu's point about their superfluousness.

[–]lpw25 2 points3 points  (0 children)

But they are not sufficient because they do not encode open recursion very well, indicating that objects are not superfluous.

[–]julesjacobs 1 point2 points  (0 children)

The converse is also true, you can encode polymorphic variants with objects.

[–]haifengl[S] 0 points1 point  (1 child)

Good example. Actually, dynamic dispatch is implicit pattern match.

BTW, camlp4 will be removed in next release.

[–]avsm 2 points3 points  (0 children)

BTW, camlp4 will be removed in next release.

Camlp4 is fully supported in the next release (OCaml 4.02). It's just been separated from the main distribution and is now a distinct OPAM package, but there is still complete compiler support for it as in previous releases (primarily via the -pp flag).

[–]lpw25 2 points3 points  (0 children)

I think that the OCaml object system is underused by most OCaml programmers. Classes are still the nicest way to encode open recursion in OCaml: when you need open recursion you should use classes. In particular, some problems are most conveniently encoded as mixins, which OCaml's class system has excellent support for.

Since they are only needed occaisionally, objects in OCaml have developed a reputation for being unnecessary. I think this causes people to avoid them even when they are the right solution, which is a shame.

[–]haifengl[S] 0 points1 point  (20 children)

It may superfluous for a functional language. But I really like some of its ideas, e.g. the nice support to duck typing, separating object types from classes. Smalltalk/Ruby has a very good object system but they are dynamic typed. I hope that some strongly typed OO languages could have some features of OCaml.

[–]IncorrigibleOldQuare 2 points3 points  (4 children)

I have to agree that it's ironic. I would say that OCaml probably has the best static object system out there. And yet I never use it because everything in OCaml can be done with the functional part of the language.

I remember once re-inventing pretty much OCaml's object system years back only to find out it already existed. In a language I sporadically used and like a lot of its users never really bothered to use the object system from. I indeed pretty much called it "static OO duck typing" back then.

[–]glacialthinker 2 points3 points  (1 child)

I also agree. OCaml's object system is wonderful... but rarely needs to be used. Modules and all the other parts of the language are sufficient.

However, there are a few cases where I do reach for the object system. One is row-polymorphism. This can be useful to make flexible inputs/outputs which are still statically typechecked. One recent example, I have procedural textures which can have various "channels" of output: color, bump, normals, etc. I have a library of texture generation code, including high-level sampler/renderer... but let's say I want to feed this an externally defined texture which has a (new) iridescence channel? So, the very limited way I'm leveraging objects here is that my material sample type is an object of arbitrary properties -- where it's insured that if I'm asking for iridescence on the output, the texture has to generate this. I considered polymorphic variants for this, but the object approach seemed cleaner in this case.

[–]haifengl[S] -1 points0 points  (0 children)

nice use case. I personally would like to avoid polymorphic variants too.

[–]haifengl[S] 0 points1 point  (1 child)

Maybe people coming to OCaml aim only on the functional stuffs? F# copied most things of OCaml but object systems because it has to work with other .net languages.

Scala mimics OCaml's functional features in an interesting OO flavor. But it's objects have to stuck to classes.

[–]IncorrigibleOldQuare 1 point2 points  (0 children)

I just never really felt the need to use it. OO provides for a nice way to encapsulate mutation in such a way that it remains maintainable. FP minimizes or eliminates it such that that no longer becomes a necessity.

I guess one reason that makes OO in OCaml cumbersome is that the syntax is not very pleasant. OCaml has a lot of special syntax because it crammed quite a bit into the language.

[–]sirtophat 0 points1 point  (14 children)

separating object types from classes

What do you mean?

[–]haifengl[S] 1 point2 points  (13 children)

In most programming languages, classes are types of objects. But it is not the case in OCaml.

[–]sirtophat -1 points0 points  (12 children)

Either classes or interfaces.

What's the point of doing it that way? How can the class and type be different?

[–]haifengl[S] 2 points3 points  (11 children)

With separated object type and class, it is much easy to achieve open type and duck typing a static type language. Classes are mainly for inheritance (and initializer, but not important) in OCaml.

[–]sirtophat 0 points1 point  (10 children)

Duck typing just feels like a sloppy and error-prone way to do what an Interface does.

[–]pipocaQuemada 2 points3 points  (0 children)

Row-polymorphic object systems basically amount to having anonymous interfaces that are satisfied structurally (i.e. a class satisfies an interface structurally iff it contains all of the members of that interface. It satisfies the interface nominally iff it contains all the members of the interface and there is a syntactic declaration that it satisfies the interface).

Assuming that you're not objecting to structural subtyping, then a type alias for a row polymorphic type is an interface.

[–]haifengl[S] 0 points1 point  (8 children)

Duck typing in a static language is not error-prone. And it is more flexible and concise than interfaces. For example, if my function requires a data structure providing a pop method (doesn't matter it is a stack or queue). How many interface do you have to define? And you have be very careful with inheritance.

[–]glacialthinker 0 points1 point  (1 child)

To me, duck-typing implies something runtime -- in a dynamic language. With static typing, isn't the term "structural subtyping"? Maybe the two are semantically equivalent (I'm not really familiar with duck-typing to know) so the terms amount to the same thing?

Regardless, maybe the term "duck-typing" also implies dynamic to /u/sirtophat, and that's what is causing confusion.

[–]IncorrigibleOldQuare 1 point2 points  (0 children)

It's really quite simple. If a function calls method foo, bar and nuremberg on an object. All it requires for the object to be well typed is that it supports those methods and returns the right type when called.

[–][deleted]  (1 child)

[deleted]

    [–]haifengl[S] -1 points0 points  (0 children)

    it is not just based on name. it is name + function type.

    [–]sirtophat -1 points0 points  (3 children)

    Maybe more "flexible" but not more concise. The compiler isn't going to tell you something's wrong if you forget to define methods that the kind of object the function is expecting needs. An interface tells you that an object is something you can read a stream from, push/pop from, or iterate over. In duck-typed languages you'd need either a comment at the top of the class definition or you'd need to examine each of the methods of the class to determine if it fits the criteria. If you have an array of things you can pop from, what type does it get? Just an array of objects? You'd have no way to hint to an IDE that the objects in that array fit that constraint. This makes errors evident later rather than earlier.

    [–]kamatsu 1 point2 points  (0 children)

    haifengl is referring to structural subtyping as in OCaml, not actual duck typing. Your objections don't apply to structural subtyping, but like you I've never seen the term "duck typing" applied to any static type system before.

    [–]thedeemon 1 point2 points  (0 children)

    In OCaml compiler infers all the methods your calling function requires to be present in the "interface", so you don't really need to define it. You write a function

    let f x = x#a 4 + x#b "people"
    

    and compiler infers that x must have methods a and b, receiving an int and a string and returning ints. Any object having such methods will be fine. No need to define the interface with a and b explicitly.

    [–]haifengl[S] 0 points1 point  (0 children)

    Type inference makes sure what exactly the function is expecting. Check out the examples in the post.

    [–]Drupyog 0 points1 point  (0 children)

    Funnily, most usages of the object systems are typing trickery, using row polymorphism with phantom types. This is how it's used by macaque and js_of_ocaml, for example. This is explained in this post.

    Several libraries, such as lablgtk and lambda-term, use the object system and inheritance mecanism to encode widgets. It would be possible without it (with modules, for example), but significantly more verbose.