Why OO Matters (in F#)

F# is a functional-first programming language that comes with a substantial object-oriented feature set. It is so feature-complete in fact, that almost any C# class can be ported over to F# code with little substantial alteration.

However significant, this subset of the language is seeing limited appreciation from the community, which I suspect is partly fuelled by the known criticisms of OOP and partly by a desire to be different than C#. After all, this is a functional-first language so we can just replace all our classes with functions. There is also the opinion that OOP in F# merely serves as a compatibility layer for .NET, so it’s really only there to cover those unfortunate scenarios of having to use a library that accepts interfaces.

Enabling Abstraction

One of the most important aspects of maintaining a nontrivial codebase is controlling complexity. Complexity can be contained by partitioning code into logically standalone components whose implementation details are hidden behind appropriately designed abstractions. In his excellent Solid and Functional article, Vesa Karvonen argues that selecting the correct abstraction is a hard problem, and that functional programming is no silver bullet in dealing with that. This resonates a lot with me, and I strongly encourage everyone to read the full article.

That said, Vesa is framing the article in Standard ML which supports a full-blown module system. Modules can be abstracted using signatures or they can be parameterized by other modules using functors. Modules are the predominant means of abstraction in the ML ecosystem. In Haskell, it is type classes and higher-kinded types. In F#, modules are intentionally stripped of any complicated features, effectively functioning as a mere namespacing construct.

My claim is that there are inherent limits to what can be expressed using just F# modules and functions, in terms of enabling good abstraction. Luckily, we can always make use of the next best thing, which is F# OO. The thesis of this article is that strategically admitting elements of OO in an F# codebase significantly improves quality and maintainability. While I cannot conclusively prove this within the confines of a single blog post, I will try to provide hints as to why this is.

Classes as Value Parametric Modules

It is often the case that an API exposed through a module must be context aware. Typically F# developers address this by adding extra parameters in every function:

module MyApi =
    let function1 dep1 dep2 dep3 arg1 = doStuffWith dep1 dep2 dep3 arg1
    let function2 dep1 dep2 dep3 arg2 = doStuffWith' dep1 dep2 dep3 arg2

While this does work well in simple cases, it does not scale nicely as dependencies increase. It would typically prompt the developer to group arguments in context records:

type MyApiContext = { Dep1 : Dep1 ; Dep2 : Dep2 ; Dep3 : Dep3 }

module MyApi =
    let function1 (ctx : MyApiContext) arg1 = doStuffWith ctx.Dep1 ctx.Dep2 ctx.Dep3 arg1
    let function2 (ctx : MyApiContext) arg2 = doStuffWith' ctx.Dep1 ctx.Dep2 ctx.Dep3 arg2

This complicates the implementation even more both in the definition site and in the consumption site. In practice, you either end up with one context type per component or one God context for the entire application. Even more importantly, this approach often violates encapsulation concerns, pushing the burden of gathering dependencies to the consumers of the API, every single time they do consume the API. Partial application also does little to address any of these concerns in nontrivial contexts.

Less experienced developers might be prompted to do something even worse: lift dependencies to module values.

module MyApi =
    let dep1 = File.ReadAllText "/Users/eirik/connectionstring.txt"
    let dep2 = Environment.GetEnvironmentVariable "DEP_2"
    let dep3 = Random().Next()

    let function1 arg = doStuffWith dep1 dep2 dep3 arg
    let function2 arg = doSutffWith dep1 dep2 dep3 arg

This is bad for many reasons: it makes the API reliant on global state, introduces unneeded side-effects, and pushes app configuration concerns deep inside the guts of our codebase. What’s more, module value initialization compiles to a static constructor for the entire compilation unit so the exact moment of execution is largely unpredictable. Initialization errors manifest as TypeInitializationExceptions which are difficult to debug.

Contrast the situation above with the elegance afforded by a plain old class:

type MyParametricApi(dep1, dep2, dep3) =
    member __.Function1 arg1 = doStuffWith dep1 dep2 dep3 arg1
    member __.Function2 arg2 = doStuffWith' dep1 dep2 dep3 arg2

An API object could be created once at application initialization, or as many times required depending on context. It’s also more amenable to testing. I should add that this approach is essentially just as “functional” as the approaches above, since it’s merely composing dependencies to expose a context-sensitive API. Importantly, it achieves this in a much simpler way both in the definition site and consumption site, which pays great dividends if realized in big codebases.

Expressive APIs

An important attribute of method-based APIs is that they allow for greater expressive power, in two important ways:

  1. Named/Optional parameters: unlike OCaml, whose functions support out-of-order named argument passing and omitted optional arguments, F# functions support neither. Luckily, we can do this using F# methods. I find this to be an immensely powerful tool when exposing non-trivially parameterizable functionality. A function that explicitly accepts 10 optional parameters is not acceptable; a method that accepts 10 optional arguments works like a charm.
  2. Method overloading: because function names like connect' and bind2 are simply not good enough when exposed in a public API.

More Powerful Types

The type system afforded by .NET is strictly more powerful than what can be expressed using modules and functions. For example, the interface

type Scheduler =
    abstract Run<'T> : Async<'T> -> 'T

encodes a kind of function that cannot be expressed in terms of proper F# lambdas. When combined with subtyping, it is possible to effectively encode existential types and Rank-N polymorphism. Even GADTs are possible, with minimal augmentations of the type system.

In practice, it is possible to leverage that additional power very effectively. In fact, F# makes it easy to define generic function literals using object expressions. This is also how the TypeShape library has been made possible.

Abstracting Modules

Functions are the unit of abstraction in F#, but that unit is often insufficient when abstracting APIs. This prompts developers to adopt an approach where abstract APIs are surfaced as either records or tuples of functions:

type Serializer =
    {
        Serialize : bool -> obj -> string
        Deserialize : bool -> string -> obj
    }

According to the F# design guidelines, use of records for building APIs is discouraged and recommends using regular interfaces instead.

I strongly agree with this recommendation for a multitude of reasons: interfaces are more powerful since they support generic methods, named arguments and optional arguments. An interface is less likely to be defined in terms of closures, making it easier to reason about when viewing from a debugger.

So the example above could be rendered as an interface like so:

type Serializer =
    abstract Serialize<'T> : preserveRefEq:bool -> value:'T -> string
    abstract Deserialize<'T> : preserveRefEq:bool -> pickle:string -> 'T

The most important aspect of this approach is readability. It is easier for a consumer of this interface to anticipate what the purpose of each argument is, in the same way that it is easier to understand a record of functions over a tuple of functions.

Representing Illegal State

A lot of proverbial ink has been spent describing how we should strive to make illegal states unrepresentable. However, I do fail to see how this could be fully realized given that the functional core of F# only consists of algebraic data types. Take for example an oft-quoted email type:

type Email = Email of string

The following values are valid instaces of type Email:

Email null
Email "John Smith"
Email "eirik@foo.bar'; DROP TABLE dbo.Users"

If we truly care about illegal states, the obvious alteration to the type above ought to be the following

type Email = private | Email of string
with
    member this.Address = let (Email addr) = this in addr
    static member TryParse(address : string) =
        if isValidEmail address then Some(Email address)
        else None

But really, this is a just a class encoded by a union. The implementation below is simpler:

type Email private (address : string) =
    member __.Address = address
    static member TryParse(address : string) =
        if isValidEmail address then Some(Email address)
        else None

NB the previous implementation might in fact be warranted in cases where free structural equality or comparison are needed. But for all intents and purposes, both approaches effectively subscribe to OO-style encapsulation.

OO And Purity

The relationship between OO and purity can be a frequent avenue for misconception. Occasionally someone will claim that by admitting objects we are ipso facto forsaking purity. On the contrary, I do claim that these really are orthogonal concerns. Just as a lambda is capable of producing side-effects, objects can be designed for purity. Good examples of this are Map and Set in the core library. The lambda is really just an abstract class with a single virtual method and lots of syntactic sugar. There is nothing fundamentally setting it apart from objects once you exclude the syntactic aspect.

Conclusions

So, is this a call to go full-blown OO in F# projects? Should we be digging up our old GoF copies? Are design patterns up there in the functional curriculum together with profunctor optics? Are inheritance and class hierarchies sexy again? No!

I am in fact proposing that there is a third way, where functional and OO components coexist, with one paradigm complementing the other. This is hardly a new idea. Quoting from the F# design guidelines:

F# is commonly called a functional-first language: object, functional and imperative paradigms are all well supported, but functional programming tends to be the first technique used. Many of the defaults of F# are set up to encourage functional programming, but programming in the other paradigms is effective and efficient, and a combination is often best of all. It is a common misconception that the functional and object programming methodologies are competing. In fact, they are generally orthogonal and largely complementary. Often, functional programming plays a stronger role “in the small” (e.g. at the implementation level of functions/method and the code contained therein) and object programming playe a bigger role “in the large” (e.g. at the structural level of classes, interfaces, and namespaces, and the organization of APIs for frameworks).

In my 6 years of working with F#, my style has gradually shifted towards embracing this approach. A few examples:

  • 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 find that method-based APIs are friendlier to consumers unfamiliar with the implementation.
  • I use records and unions for encoding internal representations and classes for encapsulating publicly visible instances. A very good example of this is the F# map implementation.
  • I rarely expose records and unions as part of a public API unless it is abundantly evident that all possible instances for the given types are valid in their context of use. This does not happen often in my experience.
  • If a module is exposed as part of a public API, care must be taken so that the number of arguments is small and behaviour can be predicted by reading the type signature of the function alone. The core List and Array modules are a good example. Avoid using modules to expose complex functionality like the Async API.

I remember reading a few years back Simon Cousins’ NOOO manifesto, which stands for Not Only Object-Oriented development. In retrospect I find this to be an excellent name for a manifesto, if only because “Not Only OO” is not the same thing as “No OO”. So here’s a proposal to revive that manifesto, perhaps with the understanding that “Not Only OO” also implies “Not Only FP” in the context of F#.

Advertisements
Why OO Matters (in F#)

30 thoughts on “Why OO Matters (in F#)

  1. Interesting to see your logic. I have gone the other way on your key decisions and hence come out with the opposite conclusion:

    1) Function parameter count problem:
    For me a function that required a large number of parameters is big code smell. It means the function is doing too much and/or there is not enough type structure. If a function takes ten parameters it should really be a network of multiple functions that each take 2-5 parameters. For me your MyApiContext should be found to have meaning in the domain and the structure of these types worked out.

    2) Records vs Interfaces for API:
    I know its recommended practice to use interfaces for a set of functions. I find this limiting and produces more boilerplate and harder to refactor. I’ve just built a hand coded serializer based on haskell Data.Serialize (https://hackage.haskell.org/package/cereal-0.5.4.0/docs/Data-Serialize.html) using records and am super pleased with it. It composes easily and gives me backward compatibility without any reflection or code gen.

    3) Representing illegal state:
    I disagree that your class is simpler when you take into account using it and functionality such as comparison, immutability and serialization.

    Anyway interesting and each to their own.

    1. eirik says:

      1) The important aspect in this point is encapsulation for me. Few things a context record can achieve in that respect. I use that pattern of course, just never in the large.

      2) Serialize uses type classes. Function records are hardly that.

      3) Definitions of “simple” vary. I wonder though how a single-case union makes serialization more amenable in this case.

      1. 2) When I say based on I just mean using the API of a Get and Put monad. No funky type class workarounds.

        3) How should I serialize your class? Which private member needs to be saved? This Email type is possibly going to become more complicated that just a string. Just look at the illegal state blog posts.

      2. eirik says:

        How should I serialize your class? Which private member needs to be saved? This Email type is possibly going to become more complicated that just a string. Just look at the illegal state blog posts.

        Surely, this question is only valid in the context of auto serializer derivation. When using picklers, like your library does, you just manually specify what needs be serialized.

      3. eirik says:

        What’s private in this case? There’s a pretty public property exposing the value and a constructor to match. The DU approach is hardly simpler. Perhaps if you look at both compiled types through ILSpy might convince you.

      4. eirik says:

        There is no way in which classes can effectively replace union types, and I never claimed the opposite. The example is merely referring to the single case DU wrapper anti-pattern. It could have been an F# record really, I just picked the more popular example.

  2. Forgot to add to 1):

    If a function takes 10 independent parameters just consider the huge permutations for testing that are required. When you think of it this way you start to see that they are not really independent parameters and the function needs to be broken up.

  3. Hey,
    I find your article pretty fascinating – the reason being, that I agree and simultaniously disagree.
    I agree with it in sofar that this is the way it is and also in sofar that many members of the F# community regard what you have written as idiomatic F# code.
    I start to disagree about the dichtonomy of records of functions vs interfaces (althou I see their uses some times) and I disagree when you start to encode business types as (mutable) classes.
    If you go that route then my question is indeed: why F# at all as you loose so much of FPs benefits – then C# with its modes mix of newly added FP features might be a better way.
    Nevertheless I understand your highlevel point which I’d like to condense (hopefully correctly) as: Use the tools that are offered to you and in the case F# those are predominantly OO-based (for programming in the large).
    Fine!
    But what is missing (and where we might disagree greatly or not) is the question: Is that a preferable state? Is it good that F# lacks so many higher level abstractions that makes FP much easier in other languages?
    Sure you somehow can encode HKTs as classes and also GADTs too, not to mention modules. All of it with significant boilerplate code minus the type safety you get when those concepts would be implemented natively.
    The more I think about it the more it becomes clear to me that F# certainly is NOT a FP “first” language. Its a FP & OO lang. Which certainly is OK and makes many people welcome.

    1. eirik says:

      I disagree when you start to encode business types as (mutable) classes.

      None of the examples I provided admit any form of mutation.

      The more I think about it the more it becomes clear to me that F# certainly is NOT a FP “first” language. Its a FP & OO lang. Which certainly is OK and makes many people welcome.

      F# is indeed a functional-first language because it has an important feature that C# will never have: it’s expression-based. It’s the precise reason why C# will never be a good scripting language. That combined with pattern matching/active patterns, HM type inference are what give F# the competitive edge over C#. So it’s a functional-first language that fully leverages the .NET type system when it comes to abstraction.

      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.

  4. Hmm, this is interesting stuff. As someone still getting their bearings wrt proper F# app and lib design, I can say that a lot of the issues you bring up resonate with me, particularly context awareness. I admit to trying all of the module/function based workarounds: extra params, context type (global and local), lifting deps, and sometimes, back to objects. Your points do help to clarify the use of objects in F# for me, but don’t you think that they may also highlight a problem with the lang itself? I am beginning to feel like I did when I began using JavaScript in earnest and read “JavaScript: The Good Parts”. Alot of the issues you describe are not things that have to do with just good style, rather, they require nuanced knowledge about how F# works, like module value initialization, or nuanced understanding of language features that in simple cases seem interchangeable but develop significant tradeoffs for advanced use, like polymorphism in modules and functions as opposed to classes/interfaces/objects/methods/etc. Any language requires a certain level of sophistication from the dev to produce good designs, but these things leave me feeling a bit more in flux than even Scala or Haskell with all their complexity (almost precisely because they are supposed to be so simple in F# yet end up not).

    1. eirik says:

      I think you make a very good point. From an architectural perspective it absolutely makes sense to understand the nuances and trade-offs, but they’re certainly not something that should concern the beginner necessarily.

      1. I guess to add a bit more to my point, in particular what is giving me the same feeling as with “JavaScript: The Good Parts” is that this nuanced use of the lang actually IS kinda important for everyday scenarios. It is not necessary for a beginner to know them to accomplish things with the lang, but nonetheless they crop up even if you are just writing a Todo app or Blog app and you are trying to figure out how to structure your logging and persistence in a way that doesn’t feel simplistic or cumbersome. The fact that the application of obvious or natural language features in many cases does not yield what might be considered a good solution exposes the developer to a level of risk and responsibility that can counteract the other benefits that the language provides. I admit, however, that I am at a loss to describe how exactly things should be instead. I suspect from what you and others have written that a lot of this has to do with how object code (which does not come as obviously or naturally as function code) can be parametrized to a degree that function code cannot? Thus people begin with function code but when they reach a point where they would like to generalize their code further they find that objects provide more capabilities for supporting their scenario and thus look to rewrite to object code which would mean giving up automatic use of things like pattern matching and partial application and other things which in turn means even more code to write, etc.? So as others seem to be suggesting, equivalent support for parametrization of object and module/function code?

      2. eirik says:

        I don’t necessarily agree that adopting OO features entails forsaking the good stuff in F#, like unions and pattern matching. I would say that my style of writing F# is still 90% functional in module/function sense, with 10% being classes that act as wrappers for the actual implementation.

  5. You reference an article on SOLID and as a counterpoint I want to reference a recent talk by Dan North proclaiming “every element of SOLID is wrong”. The point to the talk is that simple is generally better when writing and more importantly maintaining software. I get the limits of the f# module system, but I would ask if the OO solutions to these limits are really worth the hassle, or whether they simply break Dan North’s anti-SOLID mantra of “write simple code!”.

    1. eirik says:

      I’m aware of that talk, and in a way this article is in part a reaction to it. I find the statement “just write simple code” to be a platitude devoid of any real information content. Everybody starts out wanting to write “simple code”, but the road to hell is always paved with good intentions. The main fallacy perpetrated in that talk is that “simple code” is a well-defined concept, which anybody can grasp regardless of experience. In fact I claim that a simple solution (in the Hickeyan sense) can only be defined within the boundaries of a stated problem, the tooling available to solve it, a good grasp of basic principles (like SOLID!) and an acute awareness of trade-offs and other incidental complexities that may arise in what may seem like a simple solution.

  6. ankos says:

    I’m learning F# right now and can not fully wrap my head around where you would instantiate your API class. I also looked at the Map implementation and it does make sense. Also instantiating a new map withing a function seems to be ok. But lets say I have an AddressService and a CustomerService the logic itself is implemented in a functional way as you described it.
    Now an other component (in my head that is a function) wants to use these two services to relocate a customer. Where do I instantiate the services? In the component it self? That would lead to tight coupling right? Or do I hand it to the component as a dependency? Which would still not address the problem.
    In C# or Java I usually have an IoC container which takes care of providing dependencies. So, do I have to build my own composition root where I instantiate and cast the classes to an interface which I then use to do partial function application or pass it to another class? And do you use your Api classes to compose your applications or just functions? Maybe you can point me to a good example which does use your approach. Thank you.

    1. eirik says:

      The example I provided is pretty much artificial, but the point it tries to make is that class-based APIs are an excellent means for abstraction/information hiding, which requires significant boilerplate to encode using the functional approach.

      The proposal I’m making is thus this: if information hiding is appropriate, the quality of your API may benefit by utilizing objects.

      If I can give an example, here’s the public-facing API of Argu:
      https://github.com/fsprojects/Argu/blob/master/src/Argu/ArgumentParser.fs#L14

  7. In addition I prefer tupled parameters rather than curried except for the core libraries. Tupled usually works better with intellisense, is generally less typing (x:int) (y:int) vs (x:int, y:int), is more explicit, and too much currying can make for obtuse code. Terser, yes, but less intuitive. And it relieves you of having to worry about “how do I best order these parameters for optimal currying and piping”, which is orthogonal to getting your work done. My rule is that if any one of the params needs a type hint, then tuple them.

    1. eirik says:

      You make good points, although I still think that there is a place for curried functions. The fact that they still compile to to normal methods make them a good value proposition for interop too. I personally dislike mixing of styles in a single signature, i.e. curried arguments grouped in tuples.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s