Arrow "proc" notation in F#
Asked Answered
M

3

5

Is there an implementation of "proc" notation for arrows in F#? In Haskell, it looks like this:

mean2 :: Fractional a => Circuit a a
mean2 = proc value -> do
    t <- total -< value
    n <- total -< 1
    returnA -< t / n

Note the proc keyword and -< symbol.

Ideally, this would probably use computation expressions somehow, but I'm open to other approaches as well.

Miscue answered 21/4, 2021 at 18:31 Comment(4)
There is no direct equivalent of proc notation in F# - but I think you may be able to find some alternative solution. The question really is, what do you want arrows for? Is it to write more pure code, to do some functional reactive programming or something else altogether?Foah
Short answer: FRP. Longer answer: I'm looking to translate Yampa to F# and possibly use it to write a modular music synthesizer.Miscue
Hmm, then I guess the main issue is that you need to be able to write code in a way that lets you somehow restrict the expressiveness of normal F#. Would it help in any way that F# computation expressions now support applicative syntax? Maybe there is a way of keeping most of the functionality to just applicative, but adding a few extra combinators to express other arrow-y things? (It's been some time since I was looking into this, but aren't arrows just applicatives with some extra combinators?)Foah
Sorry I missed in my answer that you had started with the FsharpPlus implementationm of Arrow in the first place!Etesian
L
3

I haven't seen any equivalent notation to that in F#. I think the reason is that F# doesn't attempt to be pure and therefore doesn't have the problem that proc and -< is solving.

The equivalent F# would just be:

let mean2 input = 
    let mutable total = 0.
    let mutable count = 0.
    seq {
        for i in input do
            total <- total + i
            count <- count + 1.
            yield total / count
    }

Perhaps I have misunderstood and the proc keyword offers a lot more functionality than this simple example.

Logogriph answered 22/4, 2021 at 0:39 Comment(1)
I think this is true if you talk about just pure functions, but it would not work if you wanted to use the arrow syntax for more fancy things like functional reactive programming.Foah
E
3

UPDATE throughout this answer:

This is not Proc equivalent notation but you can write in F#:

let mean =
    let t = total << id
    let n = total << List.map (constant 1)
    listFork t (/) n 

I explain this in what follows.

I prefer J's fork conjunction which inspires the fork below - but note it is not quite the same - to do all the work in F# that, as I understand it, that Arrows would do, but this is guided by the Arrow implementation in the above mentioned F# library rather than looking at the Haskell usage (this is separate to Kliesli Arrows, I use those too, you create an infix operator for those). Anyway in 1 line:

let fork g h f a = h (g a) (f a)
  • used in conjunction with id, fst, snd, flip, tuple2, uncurry etc. as needed

Anyway the way I would use fork to create the average as described in the question:

let average values = fork List.sum (/) List.length values

or

let average = fork List.sum (/) List.length 

The running average version, using list here. (fork can be raised to different collections, here it is listFork (if extended to list collection could then be List.fork) so as common to F# unlike Haskell, need dedicated seqFork, arrayFork combinators etc.)

UPDATE: Made as similar in style to Proc as possible:

let total: int list -> float list = List.scan (+) 0 >>List.tail >> List.map float
let constant i = fun _ -> i

let fork g h f a = h (g a) (f a)
let inline listFork g h f  = fork g (List.map2 h) f 

let mean =
    let t = total << id
    let n = total << List.map (constant 1)
    listFork t (/) n 

mean [0;10;7;8];;    

> 
val total : (int list -> float list)
val constant : i:'a -> 'b -> 'a
val fork : g:('a -> 'b) -> h:('b -> 'c -> 'd) -> f:('a -> 'c) -> a:'a -> 'd
val inline listFork :
  g:('a -> 'b list) ->
    h:('b -> 'c -> 'd) -> f:('a -> 'c list) -> ('a -> 'd list)
val mean : (int list -> float list)
val it : float list = [0.0; 5.0; 5.666666667; 6.25]

It is not Proc notation obviously. Now I have the running version, I don't know whether this answers your "other approaches as well" finishing point...

Final Update:

With listFork you can make this more point-free with just a 1-liner:

let mean1 = listFork total (/) (List.map (fun _ -> 1) >> total)

What is not clear in the first example linked to in the OP, is that total is itself an arrow which does the heavy lifting of a fold or scan. That is the real power of Arrows hence their usefulness in FRP, but that is (or was) outside the scope of my usage of fork - which was more focused on point-free, composition and piping. At least till now, I will consider this further but not here!

Etesian answered 22/4, 2021 at 11:2 Comment(0)
I
0

You can indeed employ a parametrized computation expression that goes some way in the direction of Arrows. Let's first look at the basic building blocks that are used for this kind of problem (which are similarly available for FRP):

  • Seq.scan for capturing state while applying a folder to a sequence
  • Seq.zip to recombine two sequences

A naïve implementation as the following will iterate the input sequence twice, so some thought may need to be given to caching it.

let inline total source =
    Seq.scan (+) LanguagePrimitives.GenericZero source
    |> Seq.skip 1

let inline mean source =
    source
    |> Seq.map (fun _ -> LanguagePrimitives.GenericOne)
    |> total 
    |> Seq.zip (total source) 
    |> Seq.map ((<||) (/))

By modifying the total function to include a mapper we can think up a builder that iterates the input sequence twice and recombines it for the application of the final division operator.

let inline mapTotal (f : 'a->'b) source : seq<'b> =
    Seq.scan (fun s t -> f t + s) LanguagePrimitives.GenericZero source
    |> Seq.skip 1

type ZipMap<'a>(xs : seq<'a>) =
    member __.Bind(a, f) = f (a xs)
    member __.Return(op, x, y) = Seq.zip x y |> Seq.map ((<||) op)

let inline mean2 source =
    ZipMap<_>(source){
        let! t = mapTotal id
        let! n = mapTotal (fun _ -> LanguagePrimitives.GenericOne)
        return (/), t, n }

mean2 [0.; 10.; 7.; 8.]  // seq [0.0; 5.0; 5.666666667; 6.25]
mean2 [0; 10; 7; 8]      // seq [0; 5; 5; 6]
Incautious answered 24/4, 2021 at 9:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.