Building on a discussion in this question, could anyone provide code, or a link to code, showing a complete implementation of a NumericLiteralX
module (such as this one)? I'm especially interested in an efficient implementation of FromInt32
/64
for a NumericLiteralX
module that facilitates generic numeric operations. Here's a perhaps inefficient implementation taken from the aforementioned question:
module NumericLiteralG =
let inline FromZero() = LanguagePrimitives.GenericZero
let inline FromOne() = LanguagePrimitives.GenericOne
let inline FromInt32 (n:int) =
let one : ^a = FromOne()
let zero : ^a = FromZero()
let n_incr = if n > 0 then 1 else -1
let g_incr = if n > 0 then one else (zero - one)
let rec loop i g =
if i = n then g
else loop (i + n_incr) (g + g_incr)
loop 0 zero
How could this be improved and completed?
I'll just address
FromInt32
. In an ideal world, we could define it as simply aswhich would use static constraints to ensure that we could inline an explicit conversion from
int
. Unfortunately, there are two problems with this. The first is that the syntax is invalid - you can't have a concrete type (likeint
) in the "static-typars" portion of a member constraint. We can work around this by defining a helper functionSince both of these functions are inlined, this isn't any less efficient than the first attempt, it's just wordier.
Here's where we run into the second problem: this will work for real
op_Explicit
definitions (orop_Implicit
, which is treated specially by the compiler so that it is subsumed byop_Explicit
). So(10G : bigint)
will be inlined as if you had writtenSystem.Numerics.BigInt.op_Implicit 10
, which is as efficient as we can hope for. However, F# also simulatesop_Explicit
for many primitive types (e.g. for conversions fromint
tofloat
,byte
, etc.), and since the definition ofFromInt32
relies on the existence of these members it will fail at runtime (that is,(10G : float)
and even(10G : int)
will compile but will throw an exception when executed). Ideally a future version of F# might enable this to work as-is, but as of F# 2.0, we'll need to come up with a workaround.It would be nice if we could use a similar approach to how the F# core library handles this kind of problem, which would require special casing all of the implied operators but would result in everything being inlined with perfect efficiency:
However, the F# compiler rejects this with a
"Static optimization conditionals are only for use within the F# library"
message (and compiling with the secret--compiling-fslib
flag only makes things worse :)).Instead, we need to use a few additional layers of indirection to achieve something similar at runtime. First, we'll create a runtime mapping of types to conversion functions by using a static member of a generic type:
This is similar to what we were trying to achieve with the static optimization conditionals in the previous attempt, but it's deferred to runtime instead of everything being evaluated at compile time. Now we just need to define a few values to use this type:
Here, the
constrain
function is just used to make sure thatFromInt32
can only be applied to types where there's an explicit conversion from int (or one simulated by the compiler). The actual call toconstrain()
withinFromInt32
should get optimized away during compilation.With this approach,
(10G : bigint)
will get compiled to something likeIntConverterDynamicImplTable<bigint>.Result 10
, andIntConverterDynamicTable<bigint>.Result
will have a value equivalent to(System.Func<int,bigint>(bigint.op_Implicit)).Invoke
(but cached, so that the delegate is only created once). Similarly,(10G : int64)
will compile toIntConverterDynamicImplTable<int64>.Result 10
, andIntConverterDynamicTable<int64>.Result
will have a value equivalent to the conversion function(int64 : int -> int64)
, so there are overheads of a few method calls, but the overall performance should be very good.EDIT
However, if you're just looking for something more efficient than a naive implementations of
FromInt32
andFromInt64
taking time O(n), here's a version which is still relatively simple and only takes O(log n) time: