Wednesday, August 11, 2010

OOP Virtualization vs. F# Discriminated Unions

This post continues my theme of learning to think outside the OOP (object-oriented programming) box. In particular, it examines how F# discriminated unions can simplify small components in a way that captures the flavor of a hierarchy, yet without the complexity and fluff of an explicit OOP hierarchy. (Mark Pearl has an excellent post this week on a similar theme: function programming recursion vs. iterative constructs.)

Don’t get me wrong, OOP is a great paradigm, F# implements it well, and OOP is absolutely essential when working within the .NET ecosystem. It’s just that sometimes a class hierarchy can be overkill.

I picked C# to implement the OOP example, but I don’t want the takeaway from this post to be C#==Bad/F#=Good. I want to illustrate the principles, and assume readers will be more familiar with C# than with C++ or F# OOP constructs. (The example does, however, illustrate how the “ceremony” that is C#’s lineage from C can make code more difficult to read. To be fair, the brain trusts behind C# and C++ are working to reduce some of this.)

My example is a rudimentary model of logic gates. Since the inputs to the gates are fixed, it’s not particularly useful, but I wanted to keep things simple.

Here is the OOP implementation. It models the problem using an abstract base class to model a general logic gate, and concrete overrides to model And and Or gates. As you can see, there’s quite a lot of complexity and page space devoted even to this simple implementation. (As always, all the code here is presented "as-is" and without warranty or implied fitness of any kind; use at your own risk.)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ConsoleApplication3
{
  // Model.
 
  public abstract class Gate  
  {
    public List<bool> Input { get; private set; }
 
    public Gate (List<bool> input)
    {
      Input = new List<bool>(input);
    }
 
 
    public abstract bool Test ();
  }
 
 
  public class And : Gate 
  {
    public And (List<bool> input) :
      base(input) {}
 
    public override bool Test ()
    {
      return !Input.Contains(false);
    }
  }
 
 
  public class Or : Gate 
  {
    public Or (List<bool> input) :
      base(input) {}
 
    public override bool Test ()
    {
      return Input.Contains(true);
    }
  }
 
  // Example.
 
  class Program
  {
    static void Main(string[] args) 
    {
      bool baf = 
        (new And(new List<bool>() 
          {true,false,true})).Test();
 
      bool bat = 
        (new And(new List<bool>() 
          {true,true,true})).Test();
 
      bool bof = 
        (new And(new List<bool>() 
          {false,false,false})).Test();
 
      bool bot = 
        (new And(new List<bool>() 
          {false,true,false})).Test();
 
      Console.WriteLine("Your breakpoint here.");
    }
  }
}


And here is the F# discriminated union example, along with a “bonus” example showing the extension of the idea to active patterns. To me, at least, it looks a lot more straightforward and should be easier to understand when first encountered. It also takes up a lot less page space.

// Model.
 
type Gate = 
  | And of bool list
  | Or of bool list
 
let test = function
  | And(l) -> List.exists ((=) false) l |> not
  | Or(l) -> List.exists ((=) true) l 
 
// Example.
 
let baf = And([true;false;true]) |> test
let bat = And([true;true;true]) |> test
let bof = Or([false;false;false]) |> test
let bot = Or([false;true;false]) |> test
 
// Bonus extension: active pattern.
 
let (|Test|) (g:Gate) = test g
 
let fired = function | Test(b) -> b
 
let bafap = And([true;false;true]) |> fired
let batap = And([true;true;true]) |> fired
let bofap = Or([false;false;false]) |> fired
let botap = Or([false;true;false]) |> fired
 
printfn "Your breakpoint here."
 


So what *is* the takeaway? It’s this: there may be entire classes of solutions that, under the OOP paradigm, get modeled poorly or not at all, for the no other reason than that the solution adds complexity and code beyond the worth of the model. Secondarily, the same can be for linguistic “ceremony,” which – for historical reasons – often emphasizes syntax at the expense of semantics.

-Neil

p.s. The astute reader will have already noticed that there is an even more succinct representation of the test in F#. It occurred to me as I was feeding Mr. Bun-bun his dinner:

 
let test = function
  | And(l) -> List.exists not l |> not
  | Or(l) -> List.exists id l 
 

And the definitions of "Test" and "fired" can be simplified to:

 
let (|Test|) = test 
 
let fired (Test(b)) = b
 

Which makes it plain that, in this oversimplified example, "fired" is just a complicated way of calling "test," but you get the picture...

No comments: