Me limitaré a la dirección FromInt32
. En un mundo ideal, podríamos definirlo como simplemente como
let inline FromInt32 i =
((^t or int) : (static member op_Explicit : int -> ^t) i)
que utilizaría restricciones estáticas para asegurar que podríamos inline una conversión explícita de int
. Desafortunadamente, hay dos problemas con esto. La primera es que la sintaxis no es válida: no se puede tener un tipo concreto (como int
) en la parte "static-typars" de una restricción miembro. Podemos evitar esto definiendo una función auxiliar
let inline cvt i = ((^t or ^u) : (static member op_Explicit : ^u -> ^t) i)
let inline FromInt32 (i:int) = cvt i
Dado que ambas funciones son inline, esto no es menos eficiente que el primer intento, es sólo más prolijo.
Aquí es donde nos encontramos con el segundo problema: esto funcionará para las definiciones reales op_Explicit
(o op_Implicit
, que se trata especialmente por el compilador para que se subsume por op_Explicit
). Por lo tanto, (10G : bigint)
estará en línea como si hubiera escrito System.Numerics.BigInt.op_Implicit 10
, que es lo más eficiente que podemos esperar. Sin embargo, F # también simula op_Explicit
para muchos tipos primitivos (por ejemplo, para las conversiones de int
a float
, byte
, etc.), y puesto que la definición de FromInt32
se basa en la existencia de estos miembros se producirá un error en tiempo de ejecución (es decir, (10G : float)
e incluso (10G : int)
compilará pero emitirá una excepción cuando se ejecute). Idealmente, una versión futura de F # podría permitir que esto funcione como está, pero a partir de F # 2.0, tendremos que encontrar una solución alternativa.
Sería bueno si pudiéramos utilizar un enfoque similar a la forma en la biblioteca # núcleo F maneja este tipo de problemas, lo que requeriría carcasa especial a todos los operadores implicados, pero resultaría en todo lo que se inline con una eficiencia perfecta:
let inline FromInt32 (i : int) : ^t =
cvt i
when ^t : int = int i
when ^t : float = float i
when ^t : byte = byte i
...
Sin embargo, el F # compilador rechaza esto con un mensaje "Static optimization conditionals are only for use within the F# library"
(y compilar con la bandera de --compiling-fslib
secreto sólo empeora las cosas :)).
En su lugar, tenemos que usar algunas capas adicionales de direccionamiento indirecto para lograr algo similar en el tiempo de ejecución. En primer lugar, vamos a crear una asignación de tiempo de ejecución de los tipos de funciones de conversión mediante el uso de un miembro estático de un tipo genérico:
type IntConverterDynamicImplTable<'t>() =
static let result : int -> 't =
let ty = typeof< 't> //'
if ty.Equals(typeof<sbyte>) then sbyte |> box |> unbox
elif ty.Equals(typeof<int16>) then int16 |> box |> unbox
elif ty.Equals(typeof<int32>) then int |> box |> unbox
elif ty.Equals(typeof<int64>) then int64 |> box |> unbox
elif ty.Equals(typeof<nativeint>) then nativeint |> box |> unbox
elif ty.Equals(typeof<byte>) then byte |> box |> unbox
elif ty.Equals(typeof<uint16>) then uint16 |> box |> unbox
elif ty.Equals(typeof<char>) then char |> box |> unbox
elif ty.Equals(typeof<uint32>) then uint32 |> box |> unbox
elif ty.Equals(typeof<uint64>) then uint64 |> box |> unbox
elif ty.Equals(typeof<unativeint>) then unativeint |> box |> unbox
elif ty.Equals(typeof<decimal>) then decimal |> box |> unbox
elif ty.Equals(typeof<float>) then float |> box |> unbox
elif ty.Equals(typeof<float32>) then float32 |> box |> unbox
else
let m =
try ty.GetMethod("op_Implicit", [| typeof<int> |])
with _ -> ty.GetMethod("op_Explicit", [| typeof<int> |])
let del =
System.Delegate.CreateDelegate(typeof<System.Func<int,'t>>, m)
:?> System.Func<int,'t>
del.Invoke |> box |> unbox
static member Result = result
Esto es similar a lo que estábamos tratando de lograr con los condicionales de optimización estática en el intento anterior , pero se difiere al tiempo de ejecución en lugar de que todo se evalúe en tiempo de compilación.Ahora solo falta definir algunos valores de usar este tipo:
let inline constrain< ^t, ^u when (^t or ^u) : (static member op_Explicit : ^t -> ^u)>() =()
let inline FromInt32 i : ^t =
constrain<int, ^t>()
IntConverterDynamicImplTable.Result i
Aquí, la función constrain
sólo se utiliza para asegurarse de que FromInt32
sólo se puede aplicar a tipos donde hay una conversión explícita de int (o uno simulado por el compilador). La llamada real al constrain()
dentro de FromInt32
debería optimizarse durante la compilación.
Con este enfoque, (10G : bigint)
obtendrá compilado a algo así como IntConverterDynamicImplTable<bigint>.Result 10
y IntConverterDynamicTable<bigint>.Result
tendrá un valor equivalente a (System.Func<int,bigint>(bigint.op_Implicit)).Invoke
(pero en caché, por lo que el delegado sólo se crea una vez). Del mismo modo, (10G : int64)
se compilará en IntConverterDynamicImplTable<int64>.Result 10
, y IntConverterDynamicTable<int64>.Result
tendrá un valor equivalente a la función de conversión (int64 : int -> int64)
, por lo que hay gastos generales de algunas llamadas a métodos, pero el rendimiento general debería ser muy bueno.
EDITAR
Sin embargo, si estás buscando algo más eficiente que un ingenuo implementaciones de FromInt32
y FromInt64
tomar tiempo O (n), aquí está una versión que todavía es relativamente sencillo y sólo toma O (log n) tiempo:
module SymmetricOps =
let inline (~-) (x:'a) : 'a = -x
let inline (+) (x:'a) (y:'a) : 'a = x + y
let inline (-) (x:'a) (y:'a) : 'a = x - y
let inline (*) (x:'a) (y:'a) : 'a = x * y
let inline (/) (x:'a) (y:'a) : 'a = x/y
let inline (%) (x:'a) (y:'a) : 'a = x % y
module NumericLiteralG =
open SymmetricOps
let inline FromOne() = LanguagePrimitives.GenericOne
let inline FromZero() = LanguagePrimitives.GenericZero
let rec compute zero one two (/) (%) Two (+) (-) (*) pow2 rest n =
if n = zero then rest
else
let rest' =
let nmod2 = n % two
if nmod2 = zero then rest
elif nmod2 = one then rest + pow2
else rest - pow2
compute zero one two (/) (%) Two (+) (-) (*) (Two * pow2) rest' (n/two)
let inline FromInt32 i = compute 0 1 2 (/) (%) (FromOne() + FromOne()) (+) (-) (*) (FromOne()) (FromZero()) i
let inline FromInt64 i = compute 0L 1L 2L (/) (%) (FromOne() + FromOne()) (+) (-) (*) (FromOne()) (FromZero()) i
Wow. Gracias por la grandiosa explicación. – Daniel
Concediendo por el momento en que ningún simple mortal pueda implementar esto, ¿hay alguna manera más fácil de expresar un número genérico arbitrario? 'GenericZero' y' GenericOne' se dan, pero ¿qué pasa con 'GenericN'? Es esencial para operaciones numéricas genéricas ... y incómodo para calcular usando 'GenericOne' /' Zero'. – Daniel
@Daniel - Bueno, supongo que depende de qué tan eficiente sea tu necesidad. No hay nada de malo con el enfoque más directo de 'FromInt32' utilizado en su pregunta, es solo que resultará en más sobrecarga. – kvb