Why Composable: From C# to F#
In C# we define static functions:
public static int Add(int y, int x){
return x + y;
}
public static int AddOne(int x){
return Add(x, 1);
}
public static int AddTwo(int x){
return Add(x, 2);
}
When combining these functions it looks like this:
AddTwo(AddOne(4)); // 7
Not very readable.
We cram functions into objects so that code reads from left to right. Method chaining in F# and Clojure accomplishes this more flexibly. –Jessica Kerr (@jessitron) March 3, 2014
So when we do this in F#
1: 2: 3: 4: |
let add y x = x + y let addOne = add 1 let addTwo = add 2 4 |> addOne |> addTwo |
output:
1:
|
val it : int = 7 |
it works very cleanly, because the functions are in curried form.
Add
doesn't take two arguments, it takes one argument
and returns a function that takes takes 1 argument and returns a
result.
You can see manually do curried form in C# but the final invocation is still clunky.
public static Func<int,int> Add(int y){
return x => x + y;
}
var addOne = Add(1);
var addTwo = Add(2);
addTwo(addOne(4)); // 7
However there is a way to do left to right in C# without resorting to objects...Extension Methods:
public static int Add(this int x, int y){
return x + y;
}
public static int AddOne(this int x){
return x.Add(1);
}
public static int AddTwo(this int x){
return x.Add(2);
}
Giving us left to right
4.AddOne().AddTwo(); // 7
And thusly is the basis for the method chaining api of LINQ.
Enumerable.Range(1,5).Where(x => x < 3); // 1,2
In F# we already can do the equivalent
1: 2: |
seq { 1..5 } |> Seq.filter (fun x -> x < 3) |
output:
1:
|
val it : mkSeq@543<int> = seq [1; 2] |
However, if we add a little bit more complexity to the C# LINQ
new [] {
new {FirstName = "Stella", LastName = "Gibson"},
new {FirstName = "Paul", LastName = "Spector"},
new {FirstName = "Danielle", LastName = "Ferrington"},
new {FirstName = "Olivia", LastName = "Spector"},
}.OrderByDescending(x=>x.LastName).ThenBy(x=>x.FirstName);
The F# idomatic built-in list comprehension is not as robust
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: |
open System type Person = { FirstName : string; LastName: string; } let lastThenFirst (x:Person) (y:Person) = let cmp = -String.Compare(x.LastName, y.LastName) if cmp = 0 then String.Compare(x.FirstName, y.FirstName) else cmp [ {FirstName="Stella"; LastName="Gibson"} {FirstName = "Paul"; LastName = "Spector"} {FirstName = "Danielle"; LastName = "Ferrington"} {FirstName = "Olivia"; LastName = "Spector"} ] |> List.sortWith lastThenFirst |
output:
1: 2: 3: 4: 5: |
val it : list<Person> = [{FirstName = "Olivia";LastName = "Spector";}; {FirstName = "Paul"; LastName = "Spector";}; {FirstName = "Stella";LastName = "Gibson";}; {FirstName = "Danielle"; LastName = "Ferrington";}] |
You can use LINQ directly, Extension Methods are supported in F# 3.0 along with Type Directed Conversions at Member Invocations.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
open System.Linq type Person = { FirstName : string; LastName: string; } [ {FirstName="Stella"; LastName="Gibson"} {FirstName = "Paul"; LastName = "Spector"} {FirstName = "Danielle"; LastName = "Ferrington"} {FirstName = "Olivia"; LastName = "Spector"} ].OrderByDescending(fun x -> x.LastName).ThenBy(fun x -> x.FirstName) |
output:
1: 2: 3: 4: 5: |
val it : OrderedEnumerable<Person, string> = seq [{FirstName = "Olivia"; LastName = "Spector";}; {FirstName = "Paul"; LastName = "Spector";}; {FirstName = "Stella"; LastName = "Gibson";}; {FirstName = "Danielle"; LastName = "Ferrington";}] |
It's not idiomatic F# syntax. It looks like C# (just with more "fun")
But it is static, immutable and lazy.
So we have a pattern with left to right with both C# extension methods and curried form F#.
So given an extension method (this, arg) -> value
it could be reformed
into arg -> this -> value
and it would syntactically look like an F# API.
Examples
1: 2: |
let inline orderByDescending (keySelector:'TSource->'TKey) source = System.Linq.Enumerable.OrderByDescending(source, System.Func<'TSource, 'TKey>(keySelector)) let inline thenBy (keySelector:'TSource->'TKey) source = System.Linq.Enumerable.ThenBy(source, System.Func<'TSource, 'TKey>(keySelector)) |
And this is the basis for ComposableExtensions we can make .NET BCL api's that have idiomatic F# behavior look, read, write like idiomatic F# api's.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: |
open Composable.Linq type Person = { FirstName : string; LastName: string; } [ {FirstName="Stella"; LastName="Gibson"} {FirstName = "Paul"; LastName = "Spector"} {FirstName = "Danielle"; LastName = "Ferrington"} {FirstName = "Olivia"; LastName = "Spector"} ] |> Enumerable.orderByDescending (fun x -> x.LastName) |> Enumerable.thenBy (fun x -> x.FirstName) |
output:
1: 2: 3: 4: 5: |
val it : OrderedEnumerable<Person, string> = seq [{FirstName = "Olivia"; LastName = "Spector";}; {FirstName = "Paul"; LastName = "Spector";}; {FirstName = "Stella"; LastName = "Gibson";}; {FirstName = "Danielle"; LastName = "Ferrington";}] |
See also these StackOverflow questions
- Is there an F# equivalent of Enumerable.DefaultIfEmpty?
- efficient way to create map of lists in functional style
Full name: why.add
Full name: why.addOne
Full name: why.addTwo
val int : value:'T -> int (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.int
--------------------
type int = int32
Full name: Microsoft.FSharp.Core.int
--------------------
type int<'Measure> = int
Full name: Microsoft.FSharp.Core.int<_>
val seq : sequence:seq<'T> -> seq<'T>
Full name: Microsoft.FSharp.Core.Operators.seq
--------------------
type seq<'T> = System.Collections.Generic.IEnumerable<'T>
Full name: Microsoft.FSharp.Collections.seq<_>
from Microsoft.FSharp.Collections
Full name: Microsoft.FSharp.Collections.Seq.filter
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = System.String
Full name: Microsoft.FSharp.Core.string
from Microsoft.FSharp.Core
module List
from Microsoft.FSharp.Collections
--------------------
type List<'T> =
| ( [] )
| ( :: ) of Head: 'T * Tail: 'T list
interface IEnumerable
interface IEnumerable<'T>
member Head : 'T
member IsEmpty : bool
member Item : index:int -> 'T with get
member Length : int
member Tail : 'T list
static member Cons : head:'T * tail:'T list -> 'T list
static member Empty : 'T list
Full name: Microsoft.FSharp.Collections.List<_>
Full name: Microsoft.FSharp.Collections.List.sortWith
Full name: Microsoft.FSharp.Collections.list<_>
static member Aggregate<'TSource> : source:IEnumerable<'TSource> * func:Func<'TSource, 'TSource, 'TSource> -> 'TSource + 2 overloads
static member All<'TSource> : source:IEnumerable<'TSource> * predicate:Func<'TSource, bool> -> bool
static member Any<'TSource> : source:IEnumerable<'TSource> -> bool + 1 overload
static member AsEnumerable<'TSource> : source:IEnumerable<'TSource> -> IEnumerable<'TSource>
static member Average : source:IEnumerable<int> -> float + 19 overloads
static member Cast<'TResult> : source:IEnumerable -> IEnumerable<'TResult>
static member Concat<'TSource> : first:IEnumerable<'TSource> * second:IEnumerable<'TSource> -> IEnumerable<'TSource>
static member Contains<'TSource> : source:IEnumerable<'TSource> * value:'TSource -> bool + 1 overload
static member Count<'TSource> : source:IEnumerable<'TSource> -> int + 1 overload
static member DefaultIfEmpty<'TSource> : source:IEnumerable<'TSource> -> IEnumerable<'TSource> + 1 overload
...
Full name: System.Linq.Enumerable
System.Linq.Enumerable.OrderByDescending<'TSource,'TKey>(source: System.Collections.Generic.IEnumerable<'TSource>, keySelector: System.Func<'TSource,'TKey>, comparer: System.Collections.Generic.IComparer<'TKey>) : System.Linq.IOrderedEnumerable<'TSource>
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'T12,'T13,'T14,'T15,'T16,'TResult> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 * 'T12 * 'T13 * 'T14 * 'T15 * 'T16 -> 'TResult
Full name: System.Func<_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_>
--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'T12,'T13,'T14,'T15,'TResult> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 * 'T12 * 'T13 * 'T14 * 'T15 -> 'TResult
Full name: System.Func<_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_>
--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'T12,'T13,'T14,'TResult> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 * 'T12 * 'T13 * 'T14 -> 'TResult
Full name: System.Func<_,_,_,_,_,_,_,_,_,_,_,_,_,_,_>
--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'T12,'T13,'TResult> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 * 'T12 * 'T13 -> 'TResult
Full name: System.Func<_,_,_,_,_,_,_,_,_,_,_,_,_,_>
--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'T12,'TResult> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 * 'T12 -> 'TResult
Full name: System.Func<_,_,_,_,_,_,_,_,_,_,_,_,_>
--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'TResult> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 -> 'TResult
Full name: System.Func<_,_,_,_,_,_,_,_,_,_,_,_>
--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'TResult> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 -> 'TResult
Full name: System.Func<_,_,_,_,_,_,_,_,_,_,_>
--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'TResult> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 -> 'TResult
Full name: System.Func<_,_,_,_,_,_,_,_,_,_>
--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'TResult> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 -> 'TResult
Full name: System.Func<_,_,_,_,_,_,_,_,_>
--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'TResult> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 -> 'TResult
Full name: System.Func<_,_,_,_,_,_,_,_>
--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'TResult> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 -> 'TResult
Full name: System.Func<_,_,_,_,_,_,_>
--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'TResult> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 -> 'TResult
Full name: System.Func<_,_,_,_,_,_>
--------------------
type Func<'T1,'T2,'T3,'T4,'TResult> =
delegate of 'T1 * 'T2 * 'T3 * 'T4 -> 'TResult
Full name: System.Func<_,_,_,_,_>
--------------------
type Func<'T1,'T2,'T3,'TResult> =
delegate of 'T1 * 'T2 * 'T3 -> 'TResult
Full name: System.Func<_,_,_,_>
--------------------
type Func<'T1,'T2,'TResult> =
delegate of 'T1 * 'T2 -> 'TResult
Full name: System.Func<_,_,_>
--------------------
type Func<'T,'TResult> =
delegate of 'T -> 'TResult
Full name: System.Func<_,_>
--------------------
type Func<'TResult> =
delegate of unit -> 'TResult
Full name: System.Func<_>
System.Linq.Enumerable.ThenBy<'TSource,'TKey>(source: System.Linq.IOrderedEnumerable<'TSource>, keySelector: System.Func<'TSource,'TKey>, comparer: System.Collections.Generic.IComparer<'TKey>) : System.Linq.IOrderedEnumerable<'TSource>