Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Why OO Matters in F# (eiriktsarpalis.wordpress.com)
85 points by douche on March 22, 2017 | hide | past | favorite | 48 comments



I have recently given a talk that includes a comparison between OOP and parametric / higher-kinded polymorphism, the hope being to help people choose the proper abstractions in a hybrid language like Scala.

Title is "Functional Programming Inception", slides available at: https://alexn.org/blog/2017/03/15/fp-inception.html

Basically:

- OOP is about information hiding and not just when speaking of data, but also when speaking of types, so OOP is about hiding information at the type level. Thus OOP is also really good at heterogeneity, Liskov substitution principle and all that

- Parametric polymorphism is compile time, nothing gets deferred, types are very explicit and higher kinded polymorphism is about composition, with plugged-in types altering the capabilities of built instances dramatically ... in other words, it's a very different beast

In regards to F#, the problem is that in absence of OCaml functors and Haskell type-classes, plus due to lacking higher-kinded types, you're left with OOP for building abstractions. And in terms of OOP, without interfaces that can have default implementations or higher kinded types for that matter, it's not that good as an OOP language either.


An insight I found quite interesting:

oo style is great at extending types. You can add a subtype at a later date without modifying existing code and it just works.

Functional style is great at extending behavior. You can take an existing type and implement new functions on it. This means you can have a lot of functions defined on a basic data type which is amazing for composition but you can't add a new type that works with the same methods if you didn't anticipate this need.


This is called the expression problem.

http://homepages.inf.ed.ac.uk/wadler/papers/expression/expre...

http://stackoverflow.com/a/2079678 is a great SO answer on this topic.


The expression problem has a complete and rather elegant solution in mainstream OO languages with generics, invented in 2012 called object algebras:

Video: https://www.infoq.com/presentations/object-algebras

Paper: https://www.cs.utexas.edu/~wcook/Drafts/2012/ecoop2012.pdf

I understand that there is a comparable solution in FP languages called finally tagless interpreters: https://oleksandrmanzyuk.wordpress.com/2014/06/18/from-objec...


Vesa Karvonen posted a complete solution in SML here[1] (but LtU seems to be down so the link doesn't work), which I transliterated (directly, not idiomatically) to F# here[2].

[1] http://lambda-the-ultimate.org/node/2232#comment-31278

[2] http://fssnip.net/gn


"Function style" can also be OOP, FP being imo orthogonal, although it is true that in practice, with FP, polymorphism is usually achieved with parametric polymorphism because they play well together.

Speaking of parametric polymorphism, the need to modify a type's behaviour by implementing a new type is alleviated if you go with higher-kinded polymorphism, where the implementation bits are abstracted by a generic F[_] type and using type classes to go along with that.

In the example I'm giving in that presentation, I'm implementing a type named Iterant, for which the plugged-in F[_] can dramatically modify its behaviour. So in practice you don't need to implement a new kind of Iterant, which wouldn't make much sense anyway, you only need to implement a new F[_], which is used to abstract away the details of how Iterant gets evaluated, which is the part that people want to change.

Unfortunately most statically typed mainstream languages are not capable of expressing such abstractions due to lack of higher kinded types, or ways to encode / express type-classes, although this part is less problematic than lack of HKTs.


Enjoyed the article. My personal battle with f# has always been searching for what is idiomatic. Everything feels a little bit foreign when you write something in f#. One of the responses the author leaves in reply to a comment is something that I found important the more I used f#, and is something that never really gets said out loud.

"Bottom line: people need to embrace the fact that F# is a hybrid language, and that the primary motivation for using F# is the .NET platform. People interested in FP but not in .NET should just try SML or Haskell. Claiming that F# is “up there” with Haskell or SML is false advertising and ultimately deals unpleasant surprises to people coming either from .NET or Haskell backgrounds."

f# needs to find its place not only in the FP world, but also in the .NET world. That's a very hard thing to do. The language is fun, and I hope it finds its way.


IMO that is also true for Scala. And is something that gets criticized every single time I see a reference to Scala on the internet. Yet very few people acknowledge that the problem these languages are trying to solve is complex and that the solution is not always pretty because of the interaction with the platform they're supposed to run on.


I tried to learn FP by doing a project with F# but I felt like I was writing C# code with a different syntax. I would like to give FP another go with a purely functional language but then you also need libraries that enforce that approach. The .Net framework will always lead you to an OO approach.


What I found is that it took me about three tries to actually grasp functional programming. My first F# project started out the same way yours did. It was really OO in an F#. Once I looked at it, I started to realize that it was not the language, but my approach and changed it to fit the FP approach.


In the moment you get a NULL exception with a string (in a call to a .NET function) you understand that F# is part of .NET, for good and bad.


You might like the talk Thirteen ways of looking at a Turtle (https://youtu.be/AG3KuqDbmhM). Scott Wlaschin (https://fsharpforfunandprofit.com) solves the same problem 13 different ways and demonstrates a number of techniques.


F# is a training language. Only know OOP? Great, we can show you how you do the same OOP stuff you like, only using F#.

Want to learn some pure FP? Great, we can show you how to code idiomatically using pure FP.

Want to learn some pure FP but it's tough? Great, code as much in pure FP as you like, then use a big list of "Stuff you shouldn't have to do" as a way to figure out where you need to pick up some more skills.


The reason I'm using F# at the moment is indeed the platform, but not because of .Net; it's because when I tried Haskell on Windows, it didn't work, from which I conclude Haskell is primarily about Unix; I want an FP language where Windows is a first-class, no-question supported platform, and F# fits that bill.


Windows is first-class-no-questions-asked-supported by Haskell.

The same cannot be said about .NET's support for platforms other than Windows.


> Windows is first-class-no-questions-asked-supported by Haskell.

I would have hoped so, but when I tried it recently, the package managers didn't work.

http://stackoverflow.com/questions/42562702/haskell-on-windo...

http://stackoverflow.com/questions/42564296/haskell-on-windo...

> The same cannot be said about .NET's support for platforms other than Windows.

Now that is an interesting question. A few years ago when I tried compiling and running a C# program on Linux/Mono it worked fine - and I'm no Linux expert to be able to debug hairy problems, so it had to work pretty much out of the box - and I'm told things have improved since then, but for all I know, there could be problems I'm not aware of. Have you run into any?


Taking each of those in turn:

The first question is an issue because of Stack assuming something about how git works that might not be true on your system. That's probably because you have an old version of git installed. I had a similarish issue on Gentoo, where it assumes something about gcc that isn't true in the version of gcc I have installed. That's a bug in stack, it's not really Windows-specific. Try uninstalling git, installing the latest version of git, or something like that.

Your second question has nothing to do with Windows. The issue is that when one installs packages globally (as you are doing with cabal in your question), you get into issues if you have two incompatible packages. For example, you want stylish-haskell<0.6 for hfmt, and stylish-haskell requires directory<1.3, but you have 1.3 installed (because of another package).

That's called 'cabal hell'. It's basically just dependency hell for Haskell. It was common in Python before virtualenv became popular, and common in Haskell before stack became popular. In fact, a big reason for using stack is to make it easier for people new to Haskell, and people on Windows, and in particular people new to Haskell on Windows, to use Haskell. The way it gets rid of this issue is that you install dependencies locally, separately, for each project.

At the end of the day, this really sucks for you. I hope you can get it working, Haskell is a lot of fun. You should post the first issue to https://github.com/commercialhaskell/stack/issues/new.


Ah! Thanks, those are actually clearer answers than the ones on stackoverflow. It's true that I'm using a several years old version of git. I might try reinstalling it.


I have the same issues. Writing idiomatic F# is a battle. Things feel always a bit off. Its a nice language that needs more love from ots creators.


I've been thinking and reading about OOP and its roots recently.

Here are some interesting resources on the subject:

Alan Kay's elaboration on what he envisioned when he coined the term:

http://userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/doc_kay...

Two papers he references:

https://www.rand.org/content/dam/rand/pubs/research_memorand...

http://repository.cmu.edu/cgi/viewcontent.cgi?article=2281&c...

Cool article on failures of OOP:

http://www.smashcompany.com/technology/object-oriented-progr...

Its long and frustrated, but most of the points he makes are true. A lot of the stuff people claim as

I think the original vision of OOP is more valuable than what we call OOP right now. I think a good balance would be writing low-level stuff in functional style and then wrapping it in objects when you need state and cross-library messaging. Also, it seems there is some overlap between the original vision for OOP and what we call "microservices" today.


Mirror of my question from Reddit:

Don't classes cover a full superset of the functionality of ML-style functors? I have yet to find a functor-dependent construct that can't be mimicked with them, and I've found plenty of use cases of classes that can't be mimicked (or at least not without hacky workarounds) with functors. And yet it seems common for FP style programmers to lament the lack of functors in F#, but will consider it a codesmell to use classes. Am I missing something here? What does a functor do that is so taboo to do with classes?


You might be misunderstanding functors since they are basically tangential to classes. Every generic class `F<T>` where we can lift functions `T -> S` to `F<T> -> F<S>` is a functor.

Think of lists as example, they are generic over a type and we can apply functions over the base type by applying them to each element.

Functors are a basic building block of category theory. Category theory is basically the essence of composition so using functors leads to easy to reason about and extremely composable code. This also extends to things that build on functors like monads.


ML uses "functors" in a different sense than e.g. Haskell.


In ML a functor is a very different thing from Haskell. It is akin to a higher order module.

http://stackoverflow.com/a/16353711


Classes and functors are still a bit orthogonal. There are tons of things you can do with classes nowadays depending on what language you're working in, but essentially, fundamentally, functors are about types and classes are about value construction.

Many type-level things have been ported over to classes (information hiding, parameterization, sometimes even type members) but these are more clearly functor-like ideas.

So it doesn't surprise me to think that classes can emulate functors when their functionality is often built by emulating functors while functors can't emulate classes since functors as they're implemented in SML/OCaml assume that value construction will just be done by plain functions (or OCaml's class construct).

This gets back the one of the cruxes of the LSP—it's about subtyping whereas many class systems are about subclassing and those two things actually just have different behavior.

So perhaps the one thing that functors do---and the thing that if I were a F# programmer I'd guess is the thing that I'd miss---is intentionally not do things that classes do. Functors are a narrower interface which makes it simpler to reason about ("Liberties constraint, constraints liberate").


No. ML functors are about parametric polymorphism, classes are about subtype polymorphism. There are some inherent similarities between the two, but also some differences that are non-trivial to reconcile (such as early vs. late binding). C++ templates come close, but that's because they also are about parametric polymorphism, not classes as such.

ML functors are modules that are parameterized by other modules. A common use case is that the parameter module contains several types and the logic for their interaction. Unless you've got sufficiently expressive virtual types or an equivalent mechanism (C++ templates), you won't be able to approximate that with inheritance alone.

Scala in some ways approximates the expressive power of ML functors, but at the expense of being hugely more complicated. One of the virtues of ML functors is that they are fairly straightforward, both in terms of use and implementing them in a compiler.


But classes are capable of both parametric polymorphism AND subtype polymorphism.

For example, compare `class FooFunctor[T <: FooSig](arg: T)` to `module FooFunctor (arg: FooSig)`. Aren't those equivalent? What do you mean by late binding vs early binding?


First, the parametric polymorphism mechanisms in most OO languages are orthogonal to the concept of classes.

Second, as I said, try it with multiple types that are interdependent. Once you do that, you go down the rabbit hole of higher-kinded and existential types to make sure that everything lines up properly with the compiler's inference mechanisms.

A key difference is that the generics in most OO languages allow you to parameterize only by types. An ML functor makes the parameter a module. Everything you can stuff in a module can be part of the parameter signature, and you can create whatever relationships you need between them. And you can do things such as hash tables with both case-sensitive and case-insensitive string keys that still share the same string type.

Finally, in your Scala example, keep in mind that unlike ML functors, it has the usual constructor problem that many simple genericity mechanisms have: constructing instances of the parameter type. In Scala, that means you end up working with CanBuildFrom or implicit arguments; C# and Eiffel have specialized (and ultimately, still less flexible) language constructs to deal with that.


>Finally, in your Scala example, keep in mind that unlike ML functors, it has the usual constructor problem that many simple genericity mechanisms have: constructing instances of the parameter type. In Scala, that means you end up working with CanBuildFrom or implicit arguments; C# and Eiffel have specialized (and ultimately, still less flexible) language constructs to deal with that.

What does C++ have? Or what do those languages not have? Do they not work the same way as C++, where you can just write:

    auto t = T{args...};

?


Templates in C++ are basically macros and can do a lot more as a result (and that comes with the usual costs, such as your code size potentially blowing up, getting in the way of separate compilation, and the problems with enforcing constraints on parameters; concepts have yet to make it into the language). It's totally different from languages where parametric polymorphism is part of the type system proper.


Templates in C++ are not 'basically macros' in the C++ sense.


I am talking about macros in the language design sense (e.g. as in Lisp), not the narrow meaning of the C/C++ preprocessor.


That's reasonable. Although you don't get the AST, so they're not as powerful as true AST macros.


Whatever we call it, e.g "OO", inversion of control, dependency injection, polymorphism, function pointers, ...

This can all be summarized in one question: "can old code call new code?".

This is required to build a proper modular architecture (and BTW this paradigm is used by lots of C projects like the Linux kernel, VLC, ffmpeg, GPAC).

This doesn't go against the fundamental principles of FP ; as long as the type system of your language doesn't need to know "all" types (i.e including the ones you're going to your project later).


This is why I have generally moved to contract-oriented programming with dependency injection. In C# and Java this manifests as interfaces. In Rust they would be traits. I believe Haskell type classes would serve the same function. So, you can call new code from old code fine, as long as your new code implements the contract.

This was really based on the realization that the Liskov substitution principle effectively mandates that every object type is an implicit contract. Not breaking that implicit contract in polymorphic code is what LSP is about. So then the question becomes, why not just use the construct that directly corresponds to a contract instead? That way you can reserve polymorphic constructs for actual identity (IS-A) relationships and cleanly maintain SOLID principles.

EDIT: Reading a bit further down to romaniv's comment [0], I think this is possibly what Kay discusses as the ADT approach. I've never heard of that before, but based on the Wikipedia article it seems to fit the bill?

[0] https://news.ycombinator.com/item?id=13931826


OO in F# looks like fusion of multiple concepts into one:

- Ad-hoc polymorphism

- Infix function application syntax (foo.append bar instead of append foo bar)

- Product types with named members, records (class members, note that language already has records)

- C# FFI

So I'm not a fan of this "multiple responsibility" approach. For example, you can't use ad-hoc polymorphism unbundled from OOP classes. You can't use records with OOP because it's not bundled into OOP and it has its own records (class members).

"Canonical" OOP should be about message passing and not all these things.


Are you aware that records are part of OCaml, which combines with functional programming with OOP, similar to F#? https://realworldocaml.org/v1/en/html/records.html


I don't think annonymous functions have anything special to do with FP or OO for that matter. Maybe someone can fill in on this?

In my mind, functional programming is about programming with mathematical functions - Purity - No arbitrary effects being thrown around - Input to output... Ba-Da-Boom-Ba-Da-Bing.

I find this much more valuable as a restriction from the runtime than as a guideline that can be ignored whenever deemed convenient.

That being said, I wouldn't have a job if I couldn't compromise on this :(

Is there any way to enforce some purity in F#? Does the type-system annotate effectfulness?


> I don't think annonymous functions have anything special to do with FP or OO for that matter. Maybe someone can fill in on this?

Anonymous functions are of no use until functions are first-class citizens (otherwise, there's no way to call them!), and this has always been the case in FP languages.

I do agree though that the focus on mathematical "purity" is a much more defining characteristic of FP.


Closures were an integral part of Smalltalk's design.

And functions as first class citizens is not a particularly high bar. I mean, C has higher order functions.


I'm excited to use higher-order functions in C someday :)


You can do that now. Higher-order functions are simply functions that can take other functions as parameters or return them as results. C has been able to do that since forever. It's not something fancy: pretty much every programming language has something along these lines.

What is rarer is closures, but that's a different concept. You can have higher-order functions without having closures.


Haha - I'm simply stating that I'm excited to do it and not for it to be possible :P


C# & Java has anonymous functions, but not 1st-class functions AFAIK

EDIT: but then again, they truely are of no use :P hehe


When I last used F#, I had no problems with purity — it's not enforced but it's a default in standard library and it's convenient.


The thesis of this article is that strategically admitting elements of OO in an F# codebase significantly improves quality and maintainability....In my 6 years of working with F#...I typically write the implementation of a large component in the functional style behind a private module, then expose its public API as part of a standalone class.

I've been coding with F# since it came out. I don't claim to be a guru, but I've coded in various production environments, both self-architected and part of a larger group.

I write pure functional first and then refactor and abstract as necessary. I have also decided on a policy of pure microservices: stand-alone small units of functionality joined, tested, and deployed by the environment, not other toolsets.

I try to keep a completely open mind about rolling my own objects. I'm an old OO guy, I love all things OO.

Sadly, keeping to these simple principles, I find no need for objects.

At first I did. Then I got better. I keep looking for them to crop up. Instead, what happens is that I write my microservice production code in the domain language, with lots of descriptive parameters and methods. Then, as a I refactor, the code splits into "neat stuff that should be part of the type system" and "some construct that's actually solving the problem I want to solve"

So I end up with a very small amount of domain-expressive code written using symbols that are either in the type system or should be.

I'm done. That's it. Repeat and rinse -- keeping, of course, that small bit of code that actually got refactored out for re-use. This code will live in a Types.fs file. I share this file among projects.

I could be more blunt. There are no components. Once you start thinking in terms of components? You've already decided you want to use OO. There's nothing wrong with that, just don't engage in circular reasoning. If before you begin you've already decided to use OO concepts, then you're using OO concepts. F# has nothing to do with anything.

I'm still waiting for some good examples, and I'm trying to stay open-minded about it. So far, though, I haven't seen the need for them. This essay didn't change anything.


Interesting how the author avoids to mention OCaml's (far from perfect, but still) object system.


And how F# was made by OCamlers.




Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: