F#: Custom Value Types with Controlled Zero Value Semantics

So, you want to create a value type? Easy, just use the [<Struct>] attribute to mark your type. Oh, but you want a non-default constructor. Well that is okay too. Ah, but you want an immutable type that can handle the default constructor? Now that is interesting…

If you read the CLR specification then you will find out that value types implicitly support an all-zero value. This carries over into all .NET languages and you can see references to it in the F# specification as well. If you want an immutable value type that can handle this fact, then you may need to detect when a zero value construction has taken place.

Before going further, I should point out that there are some alternatives available. If you are working in a pure F# environment, then you do not need to worry about this detail in general as your code should not allow it to occur. Also, you can use the discriminated union, enumeration and record types as alternatives. However, if these types do not meet your needs (for example, you do not want the namespace polluting behaviour of a record type) then a custom value type with what I will term “controlled zero semantics” is what you need.

Zero value construction

In my case, I wanted a value type that had a single datum, an F# immutable Map. Here is the basic code:

#light

[<Struct>]
type Model(newData : Map<string, int>) =
  member s.model = newData

However, there is an automatic implicit constructor on this type and it is accessible from F# (and other .NET languages). Here is what happens in the F# interpreter:

> let m = Model();;

val m : Model = FSI_0002+Model

> m;;
val it : Model = FSI_0002+Model {model = null;}

Detecting the zero construction

Now you might think that you can just compare model to null but you cannot:

> if m.model = null then System.Console.WriteLine("model is null");;

  if m.model = null then System.Console.WriteLine("model is null");;
  -------------^^^^

stdin(8,14): error FS0043: The type 'Map<string,int>' does not have 'null' as a proper value

There is a solution, however. Most value types can also be boxed. This is the process of wrapping the value into an object instance. Note, value types are not automatically object instances and some cannot be converted. This may be a surprise to those of you that have not looked at the CLR specification or previously needed to know the distinction. However, most value types can be boxed and this will address the current need. F# provides a pervasive box for the purpose:

box : ‘a -> obj

Since null is a proper value for the type obj a comparison is valid, for example:

> if (box m.model) = null then
    System.Console.WriteLine("model is null");;
model is null
val it : unit = ()

Managing zero value semantics

Detecting a zero value construction is not generally sufficient because value types must be able to be compared. If it is possible that your type can return a semantic zero value (generally from modification of a non-zero value), then it is important that the value is returned as a true zero value representation.

Let me make this clearer. In my case, my default map has a single item in it. This is the semantic zero value that I want to use. As my type is immutable, I return a new instance of Model whenever the data is changed. Suppose that I fist add a new element and then delete that element. The simple (and incorrect) process would be as follows:

  1. let m = Model() [True zero value Model]
  2. let m2 = m.AddNode() [Detect true zero and convert to semantic zero, then add new node; result has two nodes]
  3. let m3 = m.RemoveNode() [Not true zero, just remove node; has one node and is semantic zero
  4. (m = m3) [false, content differs]

In fact, the situation is slightly worse. Here is what happens if you compare an empty Map with a null Map value:

> let m1 = Model()
let m2 = Map<string,int>.Empty
if (m1.model = m2) then
    System.Console.WriteLine("values are equal")
else
    System.Console.WriteLine("values differ");;
values differ

val m1 : Model = FSI_0002+Model
val m2 : Map<string,int>

The solution is to convert any value that is semantically zero into the zero value representation. Here is an example adapted from my code:

#light

type IE = System.Collections.IEnumerable
type IEgen<'T> = System.Collections.Generic.IEnumerable<'T>
type KVP<'K,'V> = System.Collections.Generic.KeyValuePair<'K,'V>

[<Struct>]
type Model(newModelData : Map<string,int>) =

  static member private zero_value_data =
       Map<string,int>.Empty
    |> Map.add "Initial Value" 0
    
  // model may be null
  member private s.model = newModelData
  
  // Value types have implicit zero initialisation (in CLR)
  // This is a trick to test for a null initialisation
  member private s.is_empty_model = (box s.model) = null
    
  // Need to equate an empty model with a zero value
  static member Empty with get() = new Model()

  // Cannot use Map members if Map is null
  static member private make_non_null_data (m:Model) =
    if m.is_empty_model then
      (true, Model.zero_value_data)
    else
      (false, m.model)
  
  // Create a concrete Model representation if needed
  static member private make_non_null (m:Model) =
    let (empty, newData) = m |> Model.make_non_null_data
    if empty then
      new Model(newData)
    else
      m
      
  // Accessors for the above
  member private s.full_model_data 
    with get() = s |> Model.make_non_null_data |> snd
  member private s.full_model 
    with get() = s |> Model.make_non_null
    
  // transition back to zero value if only contains root
  static member private equate_empty_value (m:Model) =
    if (m.model = Model.zero_value_data) then
      Model()
    else
      m

  member s.add key value =
       Model(   s.full_model_data
             |> Map.add key value)
    |> Model.equate_empty_value
  
  member s.remove key =
       Model(   s.full_model_data
             |> Map.remove key)
    |> Model.equate_empty_value
  
  interface IE with
    member s.GetEnumerator() = 
      (s.full_model_data
       :>IE).GetEnumerator()
  
  interface IEgen<KVP<string,int>> with
    member s.GetEnumerator() = 
      (s.full_model_data
       :>IEgen<KVP<string,int>>).GetEnumerator()

I have added IEnumerable support to allow direct, visual comparison of data. Here is some code to test zero value semantics:

let test() =
  let m1 = Model()
  let m2 = m1.add "new value" 1
  let m3 = m2.remove "new value"

  printf "m1 = %A\n" (m1 |> Seq.to_list)
  printf "m2 = %A\n" (m2 |> Seq.to_list)
  printf "m3 = %A\n" (m3 |> Seq.to_list)

  if m1 = m2 then 
    printf "Error: m1 = m2 : bad inequality semantics\n"
  else
    printf "Correct: m1 <> m2 : inequality semantics\n"

  if m2 = m3 then 
    printf "Error: m2 = m3 : bad inequality semantics\n"
  else
    printf "Correct: m2 <> m3 : inequality semantics\n"

  if m1 = m3 then 
    printf "Correct: m1 = m3 : zero value semantics\n"
  else
    printf "Error: m1 <> m3 : bad zero value semantics\n"
;;
  
do test();;

And the output from the F# interpreter:

m1 = [[Initial Value, 0]]
m2 = [[Initial Value, 0]; [new value, 1]]
m3 = [[Initial Value, 0]]
Correct: m1 <> m2 : inequality semantics
Correct: m2 <> m3 : inequality semantics
Correct: m1 = m3 : zero value semantics
val it : unit = ()

Conclusion

It is possible to create value types that can accommodate the CLR’s zero-value construction, but if the overhead of doing so is great then you should use a class type. Nevertheless, there are cases, especially when a custom type is primarily wrapping another type, when providing controlled zero value semantics may be a good solution.

As always, feedback is welcome.

Advertisements

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