10

Very often when writing generic code in F# I come by a situation similar to this (I know this is quite inefficient, just for demonstration purposes):

let isPrime n =
    let sq = n |> float |> sqrt |> int
    {2..sq} |> Seq.forall (fun d -> n % d <> 0)

For many problems I can use statically resolved types and get even a performance boost due to inlining.

let inline isPrime (n:^a) =
    let two = LanguagePrimitives.GenericOne + LanguagePrimitives.GenericOne
    let sq = n |> float |> sqrt |> int
    {two..sq} |> Seq.forall (fun d -> n % d <> LanguagePrimitives.GenericZero)

The code above won't compile because of the upper sequence limit being a float. Nongenerically, I could just cast back to int for example.

But the compiler won't let me use any of these:

  • let sq = n |> float |> sqrt :> ^a
  • let sq = n |> float |> sqrt :?> ^a

and these two lead to a InvalidCastException:

  • let sq = n |> float |> sqrt |> box |> :?> ^a
  • let sq = n |> float |> sqrt |> box |> unbox

Also, upcast and downcast are forbidden.

let sq = System.Convert.ChangeType(n |> float |> sqrt, n.GetType()) :?> ^a works, but seems very cumbersome to me.

Is there a way that I overlooked or do I really have to use the last version? Because the last one will also break for bigint, which I need quite often.

primfaktor
  • 2,831
  • 25
  • 34
  • I did some fiddling and the nicest solution I came up with was to modify the core library. If you don't particularly care about performance, you could always build up the number by adding powers of two. – John Palmer Aug 28 '14 at 12:57
  • 1
    Why do you need to cast to `float` before calling `sqrt`? – Daniel Aug 28 '14 at 13:40
  • @Daniel because you cannot (for example) pass an int to sqrt: `The type 'int' does not support the operator 'Sqrt'` – phoog Sep 08 '14 at 19:56
  • This doesn't answer the `cast` question, but you can make the function generic without having to be able to cast to float, which has the advantage of allowing it to handle values that are out of range of a float: `let inline isPrime n = let one = LanguagePrimitives.GenericOne in let rec helper factor = n / factor < factor || n % factor <> LanguagePrimitives.GenericZero && helper (factor + one) in helper (one + one)` There is room for optimization here, of course. – phoog Sep 08 '14 at 20:15

1 Answers1

4

With the trick from FsControl, we can define generic function fromFloat:

open FsControl.Core

type FromFloat = FromFloat with
    static member instance (FromFloat, _:int32 ) = fun (x:float) -> int x
    static member instance (FromFloat, _:int64 ) = fun (x:float) -> int64 x
    static member instance (FromFloat, _:bigint ) = fun (x:float) -> bigint x
let inline fromFloat (x:float):^a = Inline.instance FromFloat x

let inline isPrime (n:^a) =
    let two = LanguagePrimitives.GenericOne + LanguagePrimitives.GenericOne
    let sq = n |> float |> sqrt |> fromFloat
    {two..sq} |> Seq.forall (fun d -> n % d <> LanguagePrimitives.GenericZero)

printfn "%A" <| isPrime 71
printfn "%A" <| isPrime 6L
printfn "%A" <| isPrime 23I

Inline.instance was defined here.

Gus
  • 25,839
  • 2
  • 51
  • 76
jindraivanek
  • 226
  • 1
  • 5
  • One catch - you need to manually define the `fromFloat` overload for each numeric type. – John Palmer Aug 29 '14 at 12:00
  • 1
    You could pull this trick directly, as the author of FsControl shows e.g. [here](http://stackoverflow.com/questions/19682432/global-operator-overloading-in-f#answer-19687403): Define the static members as `$` operator and the generic function `let inline fromFloat (x : float): ^a = (FromFloat $ Unchecked.defaultof< ^a>) x` – kaefer Aug 29 '14 at 18:45
  • Ok, I finally came around to try this, and it seems to work—but I don't understand it. Could you expand on that, please (what does `Inline.instance` do, is it statically resolved or dynamically, and HOW)? – primfaktor Sep 22 '14 at 09:13
  • @kaefer: Pretty much the same holds for your hint. It's even shorter, doesn't need the `Inline` class and works. Great, but I don't get it. – primfaktor Sep 22 '14 at 09:14
  • 1
    @primfaktor I think this is basically the mechanism F# provides for generic numeric computations. Using an operator saves you from spelling out the `static member constraint invocation expression ` by hand. The compiler would implicitly translate the operator `$` (or any other binary operator) into the expression `(fun (x:^a) (y:^b) -> ((^a or ^b) : static member ($) : ^a * ^b -> ^c) (x,y))` as long as it is defined on a suitable type, here `FromFloat`. The downside is that operators won't take more than three arguments. See _14.2.2 Item-Qualified Lookup_ of the spec. – kaefer Sep 22 '14 at 15:28