Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Juxt #486

Open
OlegAlexander opened this issue Aug 4, 2022 · 9 comments
Open

Juxt #486

OlegAlexander opened this issue Aug 4, 2022 · 9 comments

Comments

@OlegAlexander
Copy link

OlegAlexander commented Aug 4, 2022

Hello and thank you for creating F#+! I'd like to propose adding juxtapose functions to the Operators module. These functions are defined as follows:

let juxt2 f g x = f x, g x
let juxt3 f g h x = f x, g x, h x
let juxt4 f g h i x = f x, g x, h x, i x
// etc

This function is called juxt in Clojure and the Python toolz library. I believe this function was first introduced by John Backus in his paper called Can programming be liberated from the von Neumann style? where it was called construction.

In Haskell and F#+ there's a similar function called sequence. Unfortunately, it returns a list and not a tuple, forcing all the result types to be the same.

juxt in combination with item and uncurry/uncurryN make point-free programming with tuples easier. Here are some examples:

open FSharpPlus

// Basic examples
let square: float -> float =
    juxt2 id id >> uncurry ( * )

let avg: list<float> -> float =
    juxt2 List.sum (List.length >> float) >> uncurry (/)

// Grouping and ungrouping tuples
let ``(a,b),c``   (a,b,c)   = (a,b),c
let ``~(a,b),c~`` ((a,b),c) = a,b,c

``(a,b),c``(1,2,3)     = (juxt2 (juxt2 item1 item2) item3)(1,2,3) // ((1, 2), 3)
``~(a,b),c~``((1,2),3) = (juxt3 (item1 >> item1) (item1 >> item2) item2)((1,2),3) // (1, 2, 3)

Please let me know if this function already exists in F#+ (or F# for that matter) and I missed it. Thank you!

@cannorin
Copy link
Member

cannorin commented Aug 4, 2022

Looks like a generalized version of &&& ("fanout" operator) in Arrow:

open FSharpPlus.Operators.Arrows

let square : float -> float =
    (id &&& id) >> uncurry ( * )

let avg : list<float> -> float =
    (List.sum &&& (List.length >> float)) >> uncurry (/)

let grouped : (int * int) * int = ((item1 &&& item2) &&& item2) (1,2,3)

We can't ungroup (int * int) * int to int * int * int with Arrow since we don't have a triple ('t1 * 't2 * 't3) version of &&&, though.

@cannorin
Copy link
Member

cannorin commented Aug 4, 2022

I've just got a working generic juxt:

type Juxt =
  static member inline Invoke (f: 'f, x) =
    let inline call_2 (a: ^a, b: ^b) = ((^a or ^b): (static member Juxt:_*_*_->_) f,a,b)
    let inline call (a: 'a, b: 'b) = call_2 (a, b)
    call (x, Unchecked.defaultof<Juxt>)

  static member inline Juxt (f: Tuple<_>, x, _: Juxt) = f.Item1 x
  static member inline Juxt ((f1, f2), x, _: Juxt) = f1 x, f2 x
  static member inline Juxt ((f1, f2, f3), x, _: Juxt) = f1 x, f2 x, f3 x
  static member inline Juxt ((f1, f2, f3, f4), x, _: Juxt) = f1 x, f2 x, f3 x, f4 x
  static member inline Juxt ((f1, f2, f3, f4, f5), x, _: Juxt) = f1 x, f2 x, f3 x, f4 x, f5 x
  static member inline Juxt ((f1, f2, f3, f4, f5, f6), x, _: Juxt) = f1 x, f2 x, f3 x, f4 x, f5 x, f6 x
  static member inline Juxt ((f1, f2, f3, f4, f5, f6, f7), x, _: Juxt) = f1 x, f2 x, f3 x, f4 x, f5 x, f6 x, f7 x
  static member inline Juxt (f: 'f, x: 't, o: ^Juxt) =
    let f1,f2,f3,f4,f5,f6,f7,frest : ('t->'u1)*('t->'u2)*('t->'u3)*('t->'u4)*('t->'u5)*('t->'u6)*('t->'u7)*'fr =
      Constraints.whenNestedTuple f
    let result =
      Tuple<_,_,_,_,_,_,_,_>(
        f1 x, f2 x, f3 x, f4 x, f5 x, f6 x, f7 x,
        ((^fr or ^Juxt): (static member Juxt: _*_*_->'ur) frest,x,o)
      ) |> retype
    let _,_,_,_,_,_,_,_ : 'u1*'u2*'u3*'u4*'u5*'u6*'u7*'ur = Constraints.whenNestedTuple result
    result

let inline juxt f x = Juxt.Invoke (f, x)

which can be used like:

let square : float -> float =
  juxt (id, id) >> uncurry ( * )

let avg: list<float> -> float =
  juxt (List.sum, List.length >> float) >> uncurry (/)

let grouped = juxt (juxt (item1, item2), item3) (1,2,3)
let ungrouped = juxt (item1 >> item1, item1 >> item2, item2) grouped

let test () =
  let f1 x = x + 1
  let f2 x = x > 0
  let f3 x = (x, -x)
  let f = (f1, f2, f3)
  let x = juxt f 42
  let g = f1, f2, f3, f1, f2, f3, f1, f2, f3
  let y1,y2,y3,y4,y5,y6,y7,y8,y9 = juxt g 42
  ()

I think we should be able to further generalize this to support Arrow.

So the questions are:

  • do we really need this in F#+?
  • do we generalize this to a single juxt or just make juxt2, juxt3, juxt4, ...?
  • do we extend this to support Arrow<'T, 'U> or should we only support 'T -> 'U?
  • what the name of this function should be?
    • juxt?
      • I don't think this name is popular among the users of other functional languages than clojure or python
    • fanoutN?
      • I have a feeling that if we call this fanoutN we should have faninN, leftN, rightN too, which would require 4X 2X effort

What do you think? @gusty @wallymathieu

@OlegAlexander
Copy link
Author

Thank you so much, @cannorin! I absolutely love your generic version. I like the name fanoutN also, but since I intend to use this function all the time, juxt is shorter. On the other hand, juxt2 is by far the most common use case and I can use the &&& operator for that. I don't mind typing fanoutN for juxt3 and higher.

@wallymathieu
Copy link
Member

wallymathieu commented Aug 4, 2022

I think it looks cool what you have done @cannorin ! 😄 I've not seen juxt or fanoutN so it's new for my part. I'll have to read more about it.

@gusty
Copy link
Member

gusty commented Aug 4, 2022

I think we can add it as fanoutN to the Tuple.fs file, which is the place so far with generalized arity functions like this one.
Of course, this if you really think the function is useful. Adding some test cases and sample usage will be the best way to prove and showcase the function.

@cannorin
Copy link
Member

cannorin commented Aug 4, 2022

There is already a 2-tuple version of fanout (=juxt2) in the global operators:

/// <summary>
/// Sends the input to both argument arrows and combine their output. Also known as the (&amp;&amp;&amp;) operator.
/// </summary>
/// <category index="8">Arrow</category>
let inline fanout (f: '``Arrow<'T,'U1>``) (g: '``Arrow<'T,'U2>``) : '``Arrow<'T,('U1 * 'U2)>`` = Fanout.Invoke f g

and it supports Arrow<'T,'U1> and Arrow<'T,'U1> instead of just 'T -> 'U1 and 'T -> 'U2.

Arrow is a generalized version of function types, which includes 'T -> 'U and Func<T, U>.

I guess fanoutN should support Arrow too (because it would be surprising if fanoutN doesn't support while fanout does), and so it should be placed in Arrow.fs instead of Tuple.fs.

Also, there is also a "reversed" version fanin:

/// <summary>
/// Splits the input between the two argument arrows and merge their outputs. Also known as the (|||) operator.
/// </summary>
/// <category index="9">Arrow Choice</category>
let inline fanin (f: '``ArrowChoice<'T,'V>``) (g: '``ArrowChoice<'U,'V>``) : '``ArrowChoice<Choice<'U,'T>,'V>`` = Fanin.Invoke f g

So I think we should probably add faninN too.

@gusty
Copy link
Member

gusty commented Aug 4, 2022

Yes, I agree in that Arrow should be supported, mainly for consistency.

@OlegAlexander
Copy link
Author

While browsing the F#+ docs, I found a few more Arrow functions that could be genericized as well. Namely, *** (which I call parallel), first, and second. *** can be genericized as parallelN. first and second can be genericized as functions up to seventh or as an nth function.

What's really interesting is that all of these functions can be derived from fanoutN.

let square x = x * x
let double x = x + x

// Parallel, first, and second
(double *** square) (3,3) // (6, 9)
first double (3,3)        // (6, 3)
second double (3,3)       // (3, 6)

// Parallel, first, and second expressed as fanouts
(double *** square) (3,3) = ((item1 >> double) &&& (item2 >> square)) (3, 3)
first double (3,3)        = ((item1 >> double) &&& item2) (3, 3)
second double (3,3)       = (item1 &&& (item2 >> double)) (3, 3)

// Dup and swap, too
(id &&& id) 5            // (5, 5)
(item2 &&& item1) (1, 2) // (2, 1)

Also, I'm trying to figure out what fanin (|||) does. Can you please provide a usage example?

@OlegAlexander
Copy link
Author

I've come up with an example of fanout3 and parallel3:

#r "nuget: FSharpPlus, 1.2.4"

open FSharpPlus
open FSharpPlus.Operators.Arrows

let fanout3 (f1, f2, f3) x = f1 x, f2 x, f3 x
let parallel3 (f, g, h) (a,b,c) = f a, g b, h c 
let toList2 (a,b) = [a;b]
let toList3 (a,b,c) = [a;b;c]

// Source: https://github.com/python/cpython/blob/main/Lib/colorsys.py
let rgb_to_yiq (r, g, b) =
    let y = 0.30 * r + 0.59 * g + 0.11 * b
    let i = 0.74 * (r - y) - 0.27 * (b - y)
    let q = 0.48 * (r - y) + 0.41 * (b - y)
    (y, i, q)

let rgb_to_yiq' =
    let calcY = toList3 >> List.map2 ( * ) [0.30; 0.59; 0.11] >> sum
    let calcI = toList2 >> List.map2 ( * ) [0.74; 0.27] >> List.reduce (-)
    let calcQ = toList2 >> List.map2 ( * ) [0.48; 0.41] >> sum
    fanout3 (calcY, item1, item3) 
    >> fanout3 (item1, (item2 &&& item1) >> uncurry (-), (item3 &&& item1) >> uncurry (-)) 
    >> fanout3 (item1, (item2 &&& item3), (item2 &&& item3))
    >> parallel3 (id, calcI, calcQ)

rgb_to_yiq(0.2, 0.6, 0.8) = rgb_to_yiq'(0.2, 0.6, 0.8) // true

If anything, this can be seen as an argument against adding these functions to F#+! But it does show that these functions can get you through some tricky point-free situations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants