7
\$\begingroup\$

It's easy to write a function that adds two ints in F#:

let add x y = x + y

Actually, it's the same as:

let add (x:int) (y:int): int = x + y

If you need to make it generic so that it can take arguments of other type than int, you should use inline keyword:

> let inline add x y = x + y;;

val inline add :
  x: ^a -> y: ^b ->  ^c
    when ( ^a or  ^b) : (static member ( + ) :  ^a *  ^b ->  ^c)

Then you can pass values of any type that has the + operator to add:

> add 1 2;;
val it : int = 3
> add 1L 2L;;
val it : int64 = 3L
> add 1I 2I;;
val it : System.Numerics.BigInteger = 3 

However, it's not easy to write a generic function if you have to use literals in the middle of an expression:

> let inline inc x = x + 1;; 

val inline inc : x:int -> int

x always evaluates to be of int since the literal 1 is of int. It would be cool if you could write something like the following pseudo-code:

let inline inc (x:^T) = x + (1 :> ^T)

In order to do so, F# provides NumericLiteralX (where X should be replaced with G, N, Z, ...):

module NumericLiteralG =
    let inline FromZero() = LanguagePrimitives.GenericZero
    let inline FromOne() = LanguagePrimitives.GenericOne
    let inline FromInt32 n =
        let one = FromOne()
        let zero = FromZero()
        let iinc = if n > 0 then 1 else -1
        let ginc = if n > 0 then one else -one

        let rec loop i g = 
            if i = n then g
            else loop (i + iinc) (g + ginc)
        loop 0 zero 

Now you can make the inc function generic as follows:

> let inline inc x = x + 1G;;

val inline inc :
  x: ^a ->  ^c
    when ( ^a or  ^b) : (static member ( + ) :  ^a *  ^b ->  ^c) and
          ^b : (static member get_One : ->  ^b)

The obvious deficiency with NumericLiteralX, however, is FromInt32 can be horribly slow for a large number. For example, if you write a function that divides a number by 1000000:

let inline divideByMillion x = x / 1000000G

the loop in FromInt32 executes 1000000 times!

Here I propose a better approach that has no such performance hit:

type BigIntCast = BigIntCast with
    static member inline (=>) (BigIntCast, x: int) = bigint x
    static member inline (=>) (BigIntCast, x: uint32) = bigint x
    static member inline (=>) (BigIntCast, x: int64) = bigint x
    static member inline (=>) (BigIntCast, x: uint64) = bigint x
    static member inline (=>) (BigIntCast, x: bigint) = x
    static member inline (=>) (BigIntCast, x: single) = bigint x
    static member inline (=>) (BigIntCast, x: double) = bigint x
    static member inline (=>) (BigIntCast, x: decimal) = bigint x
    static member inline (=>) (BigIntCast, x: byte[]) = bigint x

type NumericCast = NumericCast with
    static member inline (=>) (NumericCast, _: sbyte) = sbyte
    static member inline (=>) (NumericCast, _: byte) = byte
    static member inline (=>) (NumericCast, _: int16) = int16
    static member inline (=>) (NumericCast, _: uint16) = uint16
    static member inline (=>) (NumericCast, _: int) = int
    static member inline (=>) (NumericCast, _: uint32) = uint32
    static member inline (=>) (NumericCast, _: int64) = int64
    static member inline (=>) (NumericCast, _: uint64) = int64
    static member inline (=>) (NumericCast, _: nativeint) = nativeint
    static member inline (=>) (NumericCast, _: unativeint) = unativeint
    static member inline (=>) (NumericCast, _: single) = single
    static member inline (=>) (NumericCast, _: double) = double
    static member inline (=>) (NumericCast, _: decimal) = decimal
    static member inline (=>) (NumericCast, _: bigint) = (=>) BigIntCast

let inline (^>) t x = (NumericCast => x) t

Now the inc function can be written as:

> let inline inc x = x + (1 ^> x);;

val inline inc :
  x: ^a ->  ^c
    when ( ^a or  ^b) : (static member ( + ) :  ^a *  ^b ->  ^c) and
         (NumericCast or  ^a) : (static member ( => ) : NumericCast *  ^a ->
                                                          int ->  ^b)   

1 ^> x reads "cast 1 to the same type as x."

The results are the same as before:

> inc 1;;
val it : int = 2
> inc 1L;;
val it : int64 = 2L
> inc 1I;;
val it : System.Numerics.BigInteger = 2

Type safety is enforced by the compiler:

> inc "1";;

  inc "1";;
  ^^^

C:\Users\junyoung\AppData\Local\Temp\stdin(8,1): error FS0043: No overloads match for method 'op_EqualsGreater'. The available overloads are shown below (or in the Error List window).
Possible overload: 'static member NumericCast.( => ) : NumericCast:NumericCast * x: ^t -> ('a -> 'a) when  ^t : (static member get_One : ->  ^t)'. Type constraint mismatch. The type 
    string    
is not compatible with type
    'a    
... snip ...

In summary, it's easy to use, fast and type-safe. Choose any three. :-)

What do you think?

The original idea of using an operator trick came from here.

\$\endgroup\$
1
\$\begingroup\$

According to the comment made by Mauricio Scheffer, this has been around for quite some time, see https://github.com/gmpl/FsControl/blob/master/FsControl.Core/Numeric.fs . That code however seems to handle different versions of F# which yours doesn't.

Regarding a pure code review of your code, it does seem clean enough, and if it serves your purposes, then who are we to say otherwise. It does seem like a neat trick given a suitable use case.

My guess is however that in most cases people don't need a new function to be generic, unless they have a specific use case regarding generics. And that means that in most cases, you already know the type of your literal.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.