Tuesday, July 5, 2011

F# User-Defined Numeric Literals

Today’s blog is about something most F# programmers have perhaps never encountered: user-defined numeric literals. I won’t go into too much detail; the available documentation does a much better job than I could:

From the F# specification.

Reference manual page.

F# Power Pack. (Source code file q.fs in the math section as of this posting.)

Basically, user-defined numeric literals allow a programmer to define a new numeric type based on suffix of a literal -- similar to built-in numeric literals. (For example, instances of the built-in numeric literal type uint32 can be written as: “42u.”)

User-defined numeric literals allow the definition of new types by replacing the suffix (“u” in the uint32 example), with any of the characters Q, R, Z, I, N, or G. For example, you could write something that looks like this:

let x = 425Q

The programmer supplies a module that determines how the literals are interpreted. The name of the module must be “NumericLiteral,” followed by the suffix it controls (e.g. “NumericLiteralG”). This module must define several functions that determine how numbers of various lengths are handled. You can read about the details in the sources above, but there are five main functions:

FromZero – Called when a zero is encountered (e.g. 0Q).
FromOne – Called when a one is encountered.
FromInt32 – Numbers from 2 to Int32.MaxValue.
FromInt64 – Numbers from Int32.MaxValue+1 to Int64.MaxValue.
FromString – Even bigger numbers.

The “smallest” function that can process a number is the one that is called, and a function must exist for the proper range. For instance, if you write “42Q,” a FromInt32 function must exist, and it is the one called.

Here is a more complete example. It allows for numeric literals which translate binary representations into uint32. This is not particularly useful, since F# already defines the 0b* binary literal form, but it will do as a sample. (And as always, the code and information here are presented "as-is" and without warranty or implied fitness of any kind; use at your own risk.)

/// Numeric literal module for 32-bit unsigned 
/// binary numbers.
module NumericLiteralZ =
  let inline FromZero() = 0u
  let inline FromOne() = 1u
  let inline FromString (s:string) =
    let rec f (acc:uint32) i (s:string) =
      match i>=s.Length with
      | true -> acc
      | _ ->
      match s.[i] with
      | '0' -> f (acc*2u) (i+1) s 
      | '1' -> f (acc*2u+1u) (i+1)s 
      | _ -> failwith "Non-binary digit."
    f 0u 0 s
  let inline FromInt32 (n:int) =
  let inline FromInt64 (n:int64) = 
// Tests.
let n0 = 0Z
let n4 = 0000100Z
let n5 = 101Z
let nMax = 11111111111111111111111111111111Z
// For reference purposes, here is the 
// existing binary syntax; use it in the
// real world instead of my class above.
let n37 = 0b100101u
printfn "Your breakpoint here."

But I think this is the sort of thing that must be used with some caution. With only six suffixes available (Q, R, Z, I, N, and G), the potential for collision is high. Also, since this is an uncommon language feature, it is likely to confuse many users who encounter it for the first time.