3

I was experimenting with an implementation of Clojure Transducers in F#, and quickly hit the dreaded Value Restriction error.

The whole point of Transducers is to be composable. This is some sample code:

type Reducer<'a,'b,'c> = ('a -> 'b -> 'a) -> 'a -> 'c -> 'a

module Transducers =
   [<GeneralizableValue>]
   let inline map proj : Reducer<'result,'output,'input> =
      fun xf ->
        fun result input ->
            xf result (proj input)

   let inline conj xs x = x :: xs
   let inline toList xf input = List.fold  (xf conj) [] input

   let xform = map (fun i -> i + 9) >> map (fun a -> a * 5)
   //let xs = toList xform [1;2] // if you apply this, type will be fixed to 'a list
                                 // which makes xform unusable with eg 'a seq

Play on dotnetfiddle

GeneralizableValue was supposed to lift the value restriction, but does nothing, it seems. Your mission is to make this code compile without applying toList (Type inference will fix the type to 'a list, so you could not use the same xform with a seq) and without changing the type of xform (at least not in a way so as to make it not composable). Is this simply not possible in F#?

Mark Karpov
  • 7,499
  • 2
  • 27
  • 62
Robert Jeppesen
  • 7,837
  • 3
  • 35
  • 50

3 Answers3

4

What about annotating xform explicitly?

   [<GeneralizableValue>]
   let xform<'t> : Reducer<'t, _, _> = map (fun i -> i + 9) >> map (fun a -> a * 5) >> map (fun s -> s + 1)
desco
  • 16,642
  • 1
  • 45
  • 56
  • I've had this working, but it is simply too ugly to force on users of a lib. Imagine having to do the same when calling Seq.map. – Robert Jeppesen Sep 18 '14 at 14:25
4

Why would annotating map with [<GeneralizableValue>] affect whether xform is subject to the value restriction? (in any case, map is already generalizable since it's defined by a lambda; also I don't see the point of all the inlines).

If your requirements are:

  • xform must be generic, but not an explicitly annotated type function
  • xform is defined by the application of an operator ((>>) in this case)

then you're out of luck; xform's body is not a generalizable expression (see §14.7 in the F# spec), so the value restriction applies here.

Furthermore, I would argue that this makes sense. Imagine that the value restriction didn't apply, and that we tweaked the definition of map:

let map proj : Reducer<_,_,_> =
    printfn "Map called!"
    fun xf result input ->
        xf result (proj input)

Now enter these definitions one-by-one:

let xform<'a> : Reducer<'a,int,int> = map (fun i -> i + 9) >> map (fun a -> a * 5)

let x1 = xform (+)
let x2 = xform (*)
let x3 = xform (fun s i -> String.replicate i s)

When do you expect "Map called!" to be printed? Does the actual behavior match your expectations? In my opinion it's good that F# forces you to go out of your way to treat non-values as generic values.

So you're not going to get exactly what you want. But perhaps there's a different encoding that would work just as well for your use cases. If every reducer will be generic in the result type, then you could do this instead:

type Reducer<'b,'c> = abstract Reduce<'a> : ('a -> 'b -> 'a) -> 'a -> 'c -> 'a

module Transducers =
    let map proj =
        { new Reducer<_,_> with 
            member this.Reduce xf result input = xf result (proj input) }

    let (>!>) (r1:Reducer<'b,'c>) (r2:Reducer<'c,'d>) =
        { new Reducer<_,_> with 
            member this.Reduce xf result input = (r1.Reduce >> r2.Reduce) xf result input }

    let conj xs x = x :: xs
    let toList (xf:Reducer<_,_>) input = List.fold  (xf.Reduce conj) [] input

    let xform = map (fun i -> i + 9) >!> map (fun a -> a * 5)

Unfortunately, you've got to lift each operator like (>>) to the reducer level before you can use it, but this at least works for your example, since xform is no longer a generic value, but a non-generic value with a generic method.

kvb
  • 54,864
  • 2
  • 91
  • 133
  • Thanks! I've had something along those lines, but I really like the function composition of Clojure transducers. Everybody already knows function composition, and introducing new operators is not something I'd like to do. The assumption is that `map` won't have side effects, and value restrictions here are in place because the F# compiler can't prove that it doesn't have side effects? – Robert Jeppesen Sep 18 '14 at 21:57
  • Yes, in the presence of potential side effects something like the value restriction is necessary to maintain type safety. – kvb Sep 18 '14 at 22:15
  • Could the F# compiler do more to prove that there are no side effects, in order to relax value restrictions? – Robert Jeppesen Sep 18 '14 at 22:24
  • 1
    In theory, sure, but in practice there are some issues. First of all, how much work should the compiler do to try to verify it? The problem is undecidable so no matter what you'll have to settle for some sort of heuristic. Should the compiler inspect only source code, or should it attempt to verify purity of the IL of imported code from other DLLs? What about well known CLR intrinsics? And then whatever you decide would need to become a part of the language spec, and would be much harder for users to interpret than the current value restriction (which is quite simple). – kvb Sep 18 '14 at 22:58
3

As suggested above, and in the error message itself, can you add arguments explicitly?

let xform x = x |> map ...

F# only plays along so well with point free approaches

Sebastian Good
  • 6,310
  • 2
  • 33
  • 57