3
votes

I'm trying to define a "quasi-generic" function. Rather than making it entirely generic, I'd like it to work only for the "integer" types (i.e. byte, sbyte, int16, uint16, int, uint32, int64, uint64, bigint).

How can I put this into a type annotation on the function definition? To clarify, how would I rewrite the following code to actually work (using only 3 types presumably without loss of generalization):

let square (x: int|int64|bigint) =
    x * x
3
This might be achievable with constraints - whether or not it'd be easy/practical however, I don't know.Joe Clay

3 Answers

7
votes

First of all, there is no way to resolve such a type constraint using standard .NET generics at runtime.

F# does allow you to express a limited form of such constraints by resolving them at compile time and inserting the proper function call inline. This makes use of statically resolved type parameters.

It's quite simple for the case you have described, you can just write:

let inline square x = x * x

This will work for any type 'T which has the * operator defined.

You can also apply specific static/member constraints explicitly but this requires more ugly syntax, e.g.

let inline id item =
    ( ^T : (member Id : int) (item))

This example function will operate on any type that exposes an Id property of type int.


Update: Based on the specific use case you're describing, you really type-classes. Those don't really exist in F# (aside from a few hard-coded examples) but you can simulate them using a marker type and member constraints, here is an example:

type Marker =
    |Marker

    static member Multiply (marker : Marker, numX : int, numY : int) =
        numX * numY
    static member Multiply (marker : Marker, numX : int64, numY : int64) =
        numX * numY

let inline multiply x y =
    ((^T or ^U) : (static member Multiply : ^T * ^U * ^U -> ^S) (Marker, x, y))

multiply 5 7
multiply 5L 7L

Notice this lets you specify the exact types you want to allow the functionality on.

2
votes

There are basically 3 approaches to your problem

a) the types you are using already support the operators/methods you want to apply to them
In this case simply add inline in front of your function and be happy

b) you have full control over the types you are using
That is you can define new members at the function definition without using extension methods. In this case you define a method on each class implementing what you need

type MyInt16 = MyInt16 of int
    with
    static member Mult(x, y) = 
        match x,y with
        | MyInt16 x', MyInt16 y' -> MyInt16 (x' * y')

type MyInt32 = MyInt32 of int
    with
    static member Mult(x, y) = 
        match x,y with
        | MyInt32 x', MyInt32 y' -> MyInt32 (x' * y')

and an inline function using generic type constraints with this rather weird looking syntax

let inline mult (x:^T) (y:^T) = (^T : (static member Mult: ^T -> ^T -> ^T) (x, y))

And then you Test

let a = MyInt16 2
let b = MyInt16 3

let c = mult a b

This works. Lets see what happens when we use different types

let d = mult a (MyInt32 3)

The above will get you an error.

c) you don't have full control over your types
That is you can't define methods within the type but you would have to use extension methods. The problem with extension methods is that they can't be used with inline functions using generic type constraints. In this case you better fall back to the parameter approach I described here

type MultParam =
    | MyInt16Param of System.Int16
    | MyInt32Param of System.Int32
with 
    static member op_Implicit(x: System.Int16) = MyInt16Param x
    static member op_Implicit(x: System.Int32) = MyInt32Param x

Then define again an inline function with generic constraints that converts your incoming type into your wrapper type

let inline (!>) (x:^a) : ^b = ((^a or ^b) : (static member op_Implicit : ^a -> ^b) x)

and add your implementation. This time a bit wordier as we need to use pattern matching

let inline mult' (x: ^T) (y: ^T) : ^T =
    let x' = !> x
    let y' = !> y 
    let r = 
        match x', y' with
        | MyInt16Param x'', MyInt16Param y'' -> x'' * y'' |> box
        | MyInt32Param x'', MyInt32Param y'' -> x'' * y'' |> box
        | _ -> failwith "Not possible"
    r :?> _

Now let's test again

let e = mult' (int16(2)) (int16(3))

This works. Lets see what happens when we use different types

let f = mult' (int16(2)) (int32(3))

Error again as it should be.

Option b) is basically an emulation of Haskells type class feature where as
option c) is closer to OCamls polymorphic variants

1
votes

Constraints, shorthand for the combined effects of two features of the F# type system (Statically Resolved Type Parameters and Member Constraints), may help. Their employment allows exclusion of incompatible types at compile time, albeit with multiple, verbose error messages.

module MyInt =
    type MyInt<'T> = private MyInt of 'T
    type Wrap = Wrap with
        static member ($) (Wrap, value : int   ) = MyInt value
        static member ($) (Wrap, value : int64 ) = MyInt value
        static member ($) (Wrap, value : bigint) = MyInt value
    let inline create value : MyInt<_> = Wrap $ value

let x = MyInt.create 1 // MyInt.MyInt<int>
let y = MyInt.create 1I // MyInt.MyInt<bigint>
let z = MyInt.create 1.0 // Error  No overloads match for method 'op_Dollar'. ...

If it's inconvenient to attach constraints on entry to the specialized domain, this can also be done when leaving.

module MyInt' =
    type MyInt<'T> = private MyInt of 'T
    type Unwrap = Unwrap with
        static member ($) (Unwrap, MyInt(value : int   )) = value
        static member ($) (Unwrap, MyInt(value : int64 )) = value
        static member ($) (Unwrap, MyInt(value : bigint)) = value
    let inline myInt value = Unwrap $ value
    let x, y, z = MyInt 1, MyInt 1I, MyInt 1.0

let a = MyInt'.myInt MyInt'.x // int
let b = MyInt'.myInt MyInt'.y // bigint
let c = MyInt'.myInt MyInt'.z // Error  No overloads match for method 'op_Dollar'. ...