Multiple items
val char : char

--------------------
type char = System.Char

Full name: Microsoft.FSharp.Core.char
val zero : int
namespace System
type Convert =
  static val DBNull : obj
  static member ChangeType : value:obj * typeCode:TypeCode -> obj + 3 overloads
  static member FromBase64CharArray : inArray:char[] * offset:int * length:int -> byte[]
  static member FromBase64String : s:string -> byte[]
  static member GetTypeCode : value:obj -> TypeCode
  static member IsDBNull : value:obj -> bool
  static member ToBase64CharArray : inArray:byte[] * offsetIn:int * length:int * outArray:char[] * offsetOut:int -> int + 1 overload
  static member ToBase64String : inArray:byte[] -> string + 3 overloads
  static member ToBoolean : value:obj -> bool + 17 overloads
  static member ToByte : value:obj -> byte + 18 overloads
  ...

Full name: System.Convert
System.Convert.ToInt32(value: System.DateTime) : int
   (+0 other overloads)
System.Convert.ToInt32(value: string) : int
   (+0 other overloads)
System.Convert.ToInt32(value: decimal) : int
   (+0 other overloads)
System.Convert.ToInt32(value: float) : int
   (+0 other overloads)
System.Convert.ToInt32(value: float32) : int
   (+0 other overloads)
System.Convert.ToInt32(value: uint64) : int
   (+0 other overloads)
System.Convert.ToInt32(value: int64) : int
   (+0 other overloads)
System.Convert.ToInt32(value: int) : int
   (+0 other overloads)
System.Convert.ToInt32(value: uint32) : int
   (+0 other overloads)
System.Convert.ToInt32(value: uint16) : int
   (+0 other overloads)
type Char =
  struct
    member CompareTo : value:obj -> int + 1 overload
    member Equals : obj:obj -> bool + 1 overload
    member GetHashCode : unit -> int
    member GetTypeCode : unit -> TypeCode
    member ToString : unit -> string + 1 overload
    static val MaxValue : char
    static val MinValue : char
    static member ConvertFromUtf32 : utf32:int -> string
    static member ConvertToUtf32 : highSurrogate:char * lowSurrogate:char -> int + 1 overload
    static member GetNumericValue : c:char -> float + 1 overload
    ...
  end

Full name: System.Char
System.Char.IsDigit(c: char) : bool
System.Char.IsDigit(s: string, index: int) : bool
union case Option.Some: Value: 'T -> Option<'T>
union case Option.None: Option<'T>
val parseScore : chars:char list -> Option<int> list

Full name: Index.parseScore
val chars : char list
type 'T list = List<'T>

Full name: Microsoft.FSharp.Collections.list<_>
Multiple items
val char : value:'T -> char (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.char

--------------------
type char = System.Char

Full name: Microsoft.FSharp.Core.char
module Option

from Microsoft.FSharp.Core
Multiple items
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 rest : char list
active recognizer Digit: char -> int option

Full name: Index.( |Digit|_| )
val x : int
val parseScoreResult : Option<int> list

Full name: Index.parseScoreResult
val countScore : scores:int list -> int

Full name: Index.countScore
val scores : int list
val count : (int -> int list -> int)
val frame : int
val b1 : int
val b2 : int
val next : int list
val r1 : int
val r2 : int
val countScoreResult : int

Full name: Index.countScoreResult
val sequence : optionals:'a option list -> 'a list option

Full name: Index.sequence
val optionals : 'a option list
type 'T option = Option<'T>

Full name: Microsoft.FSharp.Core.option<_>
val sequence' : ('b list option -> 'b option list -> 'b list option)
val acc : 'b list option
val optionals : 'b option list
val map : mapping:('T -> 'U) -> option:'T option -> 'U option

Full name: Microsoft.FSharp.Core.Option.map
Multiple items
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
  interface IEnumerable
  interface IEnumerable<'T>
  member GetSlice : startIndex:int option * endIndex:int option -> 'T list
  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<_>
val rev : list:'T list -> 'T list

Full name: Microsoft.FSharp.Collections.List.rev
val h : 'b
val t : 'b option list
val acc : 'b list
val oneOption : string list option

Full name: Index.oneOption
val bowlingScore : score:string -> Option<int>

Full name: Index.bowlingScore
val score : string
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = System.String

Full name: Microsoft.FSharp.Core.string
System.String.ToCharArray() : char []
System.String.ToCharArray(startIndex: int, length: int) : char []
module Array

from Microsoft.FSharp.Collections
val toList : array:'T [] -> 'T list

Full name: Microsoft.FSharp.Collections.Array.toList
val bowlingScoreResult : Option<int>

Full name: Index.bowlingScoreResult
Multiple items
type EntryPointAttribute =
  inherit Attribute
  new : unit -> EntryPointAttribute

Full name: Microsoft.FSharp.Core.EntryPointAttribute

--------------------
new : unit -> EntryPointAttribute
val printfn : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
type byref<'T> = (# "<Common IL Type Omitted>" #)

Full name: Microsoft.FSharp.Core.byref<_>
type bool = System.Boolean

Full name: Microsoft.FSharp.Core.bool
type Int32 =
  struct
    member CompareTo : value:obj -> int + 1 overload
    member Equals : obj:obj -> bool + 1 overload
    member GetHashCode : unit -> int
    member GetTypeCode : unit -> TypeCode
    member ToString : unit -> string + 3 overloads
    static val MaxValue : int
    static val MinValue : int
    static member Parse : s:string -> int + 3 overloads
    static member TryParse : s:string * result:int -> bool + 1 overload
  end

Full name: System.Int32
System.Int32.TryParse(s: string, result: byref<int>) : bool
System.Int32.TryParse(s: string, style: System.Globalization.NumberStyles, provider: System.IFormatProvider, result: byref<int>) : bool
val dict : keyValuePairs:seq<'Key * 'Value> -> System.Collections.Generic.IDictionary<'Key,'Value> (requires equality)

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.dict
namespace System.Collections
namespace System.Collections.Generic
Multiple items
type Dictionary<'TKey,'TValue> =
  new : unit -> Dictionary<'TKey, 'TValue> + 5 overloads
  member Add : key:'TKey * value:'TValue -> unit
  member Clear : unit -> unit
  member Comparer : IEqualityComparer<'TKey>
  member ContainsKey : key:'TKey -> bool
  member ContainsValue : value:'TValue -> bool
  member Count : int
  member GetEnumerator : unit -> Enumerator<'TKey, 'TValue>
  member GetObjectData : info:SerializationInfo * context:StreamingContext -> unit
  member Item : 'TKey -> 'TValue with get, set
  ...
  nested type Enumerator
  nested type KeyCollection
  nested type ValueCollection

Full name: System.Collections.Generic.Dictionary<_,_>

--------------------
System.Collections.Generic.Dictionary() : unit
System.Collections.Generic.Dictionary(capacity: int) : unit
System.Collections.Generic.Dictionary(comparer: System.Collections.Generic.IEqualityComparer<'TKey>) : unit
System.Collections.Generic.Dictionary(dictionary: System.Collections.Generic.IDictionary<'TKey,'TValue>) : unit
System.Collections.Generic.Dictionary(capacity: int, comparer: System.Collections.Generic.IEqualityComparer<'TKey>) : unit
System.Collections.Generic.Dictionary(dictionary: System.Collections.Generic.IDictionary<'TKey,'TValue>, comparer: System.Collections.Generic.IEqualityComparer<'TKey>) : unit
type IDisposable =
  member Dispose : unit -> unit

Full name: System.IDisposable

F# CAMP

Writing .NET applications in F#

  • Open up new instance of Visual Studio 2015
  • Make sure you have F# templates (in Other Languages)
  • Let's stick to .NET Framework v4.5.1

fsharp_templates.png

Agenda

  • F# Library (bowling score)
  • F# Console app
  • F# Build script - FAKE
  • F# Test project - xUnit
  • C# Window app - WPF (integration with F#)
  • F# Web app

F# Library (bowling score)

  • Create new, blank solution called "bowling" in directory of your choice

blank_sln.png

  • Create new F# library called "bowling" in the solution
  • Delete Script.fsx from the project
  • Rename Library1.fs to Bowling.fs
  • Open renamed file Bowling.fs in editor
  • Remove generated code from the file, and insert module declaration:
1: 
module Bowling

! Remember to save all changes when manipulating projects in Visual Studio (Ctrl + Shift + S)

  • Copy code for Digit active pattern recognizer into Bowling module
1: 
2: 
3: 
4: 
5: 
6: 
let (|Digit|_|) char =
    let zero = System.Convert.ToInt32 '0'
    if System.Char.IsDigit char then
        Some (System.Convert.ToInt32 char - zero)
    else
        None
  • Copy code for parseScore function after Digit
1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
let rec parseScore (chars: list<char>) : list<Option<int>> =
    match chars with
    | [] -> []
    | 'X' :: rest -> Some 10 :: parseScore rest
    | Digit x :: '/' :: rest -> Some x :: Some (10 - x) :: parseScore rest
    | Digit x :: rest -> Some x :: parseScore rest
    | '-' :: '/' :: rest -> Some 0 :: Some 10 :: parseScore rest
    | '-' :: rest -> Some 0 :: parseScore rest
    | _ :: rest -> None :: parseScore rest

Test the code in Interactive

  • Select all lines excluding module declaration
  • Trigger Execute in Interactive
  • In interactive window, enter following:
1: 
let parseScoreResult = parseScore ['4'; '/'];;

Value of parseScoreResult

[Some 4; Some 6]

Exercise

  • Extend Digit active pattern to recognize '-' character as well,
  • Rename Digit to Pins to better reflect its intent after the change,
  • Refactor parseScore function - make use of the fact that Pins recognizes now '-' and remove redundant pattern matching case(s),
  • In interactive, make sure that after refactoring the code still works.
  • Add countScore function
 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
let countScore (scores: list<int>) : int =
    let rec count frame scores =
        match scores with 
        | [] -> 0
        | 10 :: (b1 :: b2 :: _ as next) ->
            10 + b1 + b2 + (if frame = 10 then 0 else count (frame+1) next)
        | r1 :: r2 :: (b1 :: _ as next) when r1 + r2 = 10 ->
            10 + b1 +      (if frame = 10 then 0 else count (frame+1) next)
        | r1 :: r2 :: next ->
            r1 + r2 + count (frame+1) next

    count 1 scores
  • Test the function in interactive:
1: 
let countScoreResult = countScore [10;9;1;5;5;7;2;10;10;10;9;0;8;2;9;1;10];;

Value of countScoreResult

187
  • Add sequence function
 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
let sequence (optionals: list<option<'a>>) : option<list<'a>> =
    let rec sequence' acc optionals =
        match optionals, acc with
        | [],_ -> 
            Option.map List.rev acc
        | Some h :: t, Some acc -> 
            sequence' (Some (h :: acc)) t
        | _ -> 
            None

    sequence' (Some []) optionals
  • Test the function in interactive:
1: 
let oneOption = sequence [Some "abc"; Some "def"; Some "ghi"];;

Value of oneOption

Some ["abc"; "def"; "ghi"]
  • Add bowlingScore function
1: 
2: 
3: 
4: 
5: 
6: 
let bowlingScore (score: string) : Option<int> =
    score.ToCharArray()
    |> Array.toList
    |> parseScore
    |> sequence
    |> Option.map countScore
  • Test the function in interactive:
1: 
let bowlingScoreResult = bowlingScore "X9/5/72XXX9-8/9/X";;

Value of bowlingScoreResult

Some 187

Summary

  • Creating F# Library projects in VS
  • Declaring Bowling module
  • Testing code in interactive

Links

F# Console app

  • Create new F# Console Application "bowling.console"
  • Add project reference from "bowling.console" to "bowling"
  • Compile "bowling" project
  • Invoke Bowling.bowlingScore on example input and print output
1: 
2: 
3: 
4: 
5: 
6: 
7: 
// Learn more about F# at http://fsharp.org
// See the 'F# Tutorial' project for more help.

[<EntryPoint>]
let main argv = 
    printfn "%A" (Bowling.bowlingScore "XXXXXXXXXXXX")
    0 // return an integer exit code
  • Run it!

some_300.png

Exercise

Invoke Bowling.bowlingScore for each argument from argv (console arguments)

XXXXXXXXXXXXX 9-9-9-9-9-9-9-9-9-9- 5/5/5/5/5/5/5/5/5/5/5 X9/5/72XXX9-8/9/X

array_iter.png

Hint: Use Array.iter function to perform an action for each element from an array

Summary

  • Creating F# console apps
  • Printing to console

Links

F# Build script - FAKE

Paket for managing dependencies

  • Create new directory ".paket" next to the ".sln" solution file
  • Download paket.bootstrapper.exe from here and save it in ".paket" directory
  • In console, change directory to where the solution file and ".paket" folder are located. Do not change directory to ".paket"
  • Run paket.bootstrapper.exe from console to download newest Paket, and then invoke paket.exe init:
1: 
2: 
> .paket\paket.bootrapper.exe
> .paket\paket.exe init
  • In solution, add "New Solution Folder" called ".project"
  • "Add Existing Item" - add "paket.dependencies" file to the ".project" solution folder
  • Open "paket.dependencies" file in the VS editor

Modify the "paket.dependencies" file to add "Build" group and "FAKE" package:

1: 
2: 
3: 
4: 
group Build
    source https://www.nuget.org/api/v2

    nuget FAKE

Run paket install:

1: 
> .paket\paket.exe install

Add "New Item", "build.cmd" to the ".project" solution folder:

1: 
2: 
3: 
4: 
5: 
@echo off
cls
.paket\paket.bootstrapper.exe
.paket\paket.exe restore
packages\Build\FAKE\tools\FAKE.exe build.fsx %*

Add "New Item", "build.fsx" to the ".project" solution folder:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
#r @"packages/Build/FAKE/tools/FakeLib.dll"

open Fake

Target "Build" (fun _ ->
    MSBuildRelease "bin" "Build" ["bowling.sln"]
    |> Log "Build output"
)

RunTargetOrDefault "Build"

Run the build script:

1: 
build.cmd

build_hello_fake.png

Summary

  • Paket for managing dependencies
  • FAKE for build scripts
  • Invoking build script from command line

Links

F# Test project - xUnit

  • Create new F# Library "bowling.tests" for .NET 4.5.1,
  • Remove "Script.fsx" file,
  • Rename "Library1.fs" to "Tests.fs",
  • "Add new item", "App.config" application configuration file to "bowling.tests",
  • Add Project Reference from "bowling.tests" to "bowling",
  • Remove boilerplate code and declare Bowling.Tests module:
1: 
module Bowling.Tests

! Save all changes in Visual Studio

  • Open "paket.dependencies" in VS editor,
  • Add "xunit.runner.console" package to "Build" group,
  • Add new group "Tests" with "framework: net451",
  • Add "FSharp.Core" 4.0.0.1 with "redirects: force" option, "xUnit" and "FsUnit.xUnit" nugets to "Tests" group
 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
group Build
    source https://www.nuget.org/api/v2

    nuget FAKE
    nuget xunit.runner.console

group Tests
    framework: net451
    source https://www.nuget.org/api/v2
    
    nuget FSharp.Core 4.0.0.1 redirects: force
    nuget xUnit
    nuget FsUnit.xUnit
  • Add "New Item", "paket.references" (General -> Text File) to "bowling.tests" project
  • Open "paket.references" file in VS editor and fill it with below:
1: 
2: 
3: 
4: 
group Tests
    FSharp.Core
    xUnit
    FsUnit.xUnit

Run "paket install":

1: 
> .paket\paket.exe install
  • Open "build.fsx" build script in VS editor,
  • Add "Tests" build target:
1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
open Fake.Testing // for testing helper functions

Target "Tests" (fun _ ->
    ["bin/bowling.tests.dll"]
    |> xUnit2 (fun xunitParams -> 
        { xunitParams with ToolPath = @"packages/Build/xunit.runner.console/" 
                                    + @"tools/xunit.console.exe" }
    )
)

At the bottom of "build.fsx", specify Target dependency and change default target to "Tests":

1: 
2: 
3: 
4: 
5: 
6: 
// Targets above    

"Build"
    ==> "Tests"

RunTargetOrDefault "Tests"
 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
#r @"packages/Build/FAKE/tools/FakeLib.dll"

open Fake
open Fake.Testing

Target "Build" (fun _ ->
    MSBuildRelease "bin" "Build" ["bowling.sln"]
    |> Log "Build output"
)

Target "Tests" (fun _ ->
    ["bin/bowling.tests.dll"]
    |> xUnit2 (fun xunitParams -> 
        { xunitParams with ToolPath = @"packages/Build/xunit.runner.console/" 
                                    + @"tools/xunit.console.exe" }
    )
)

"Build"
    ==> "Tests"

RunTargetOrDefault "Tests"
  • Open "Tests.fs" source file in editor,
  • Add unit test for checking score of 12 Strikes:
 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
module Bowling.Tests

open Xunit
open FsUnit.Xunit
    
[<Fact>]
let ``12 strikes in row`` () =
    let expected = Some 270
    let actual = Bowling.bowlingScore "XXXXXXXXXXXX"
    actual |> should equal expected

Run the build script (without any additional parameters):

test_failure.png

Exercises

  • Fix the test
  • Add three more test cases for following scores:
    • "9-9-9-9-9-9-9-9-9-9-"
    • "5/5/5/5/5/5/5/5/5/5/5"
    • "X9/5/72XXX9-8/9/X"

Summary

  • Creating test library in F#
  • Adding test nuget packages with Paket
  • Attaching tests to the build script pipeline
  • Writing unit tests in F#

Links

C# Window app - WPF (integration with F#)

  • Add C# Windows "WPF Application" project, "bowling.wpf" to the solution,
  • Add project reference from "bowling.wpf" to "bowling",
  • Design awesome GUI with a TextBox, TextBlock and a Button:

gui.png

  • Open "paket.dependencies" file,
  • Add "FSharp.Core" 4.0.0.1 package with "redirects: force" option to main group,
  • Use "framework: net451" for main group as well:
 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
framework: net451
source https://www.nuget.org/api/v2

nuget FSharp.Core 4.0.0.1 redirects: force

group Build
    source https://www.nuget.org/api/v2

    nuget FAKE
    nuget xunit.runner.console

group Tests
    framework: net451
    source https://www.nuget.org/api/v2
    
    nuget FSharp.Core 4.0.0.1 redirects: force
    nuget xUnit
    nuget FsUnit.xUnit

Add "New Item", "paket.references" to "bowling.wpf":

1: 
FSharp.Core

Run paket install:

1: 
> .paket\paket.exe install

Add action on button click:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
private void button_Click(object sender, RoutedEventArgs e)
{
    var input = textBox.Text;
    var score = Bowling.bowlingScore(input);
    textBlock.Text = 
        FSharpOption<int>.get_IsSome(score) ? 
        "Score: " + score.Value.ToString() : 
        "Wrong score!";
}

Run it!

wpf_runit.png

The F# Component Design Guidelines

Below snippet doesn't feel nice in C#:

1: 
2: 
3: 
4: 
textBlock.Text = 
    FSharpOption<int>.get_IsSome(score) ? 
    "Score: " + score.Value.ToString() : 
    "Wrong score!";

The F# Component Design Guidelines

http://fsharp.org/specs/component-design-guidelines/

✔ Consider using the TryGetValue pattern instead of returning F# option values (option) in vanilla .NET APIs, and prefer method overloading to taking F# option values as arguments.

This tick can be found in this section of above guidelines.

Exercise

Create new function TryGetBowlingScore in Bowling module for better interop with C#, conforming to the F# Component Design Guidelines. Use the new function in code behind button click in C#.

Skeleton of the function

1: 
2: 
3: 
let TryGetBowlingScore(score: string, result : byref<int>) : bool = 
    result <- 0
    false

New stuff - assign value operator

1: 
2: 
let mutable x = 0
x <- 5

Using .NET libraries from F#

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
let (i1success,i1) = System.Int32.TryParse("123");
if i1success then printfn "parsed as %i" i1 else printfn "parse failed"

let dict = new System.Collections.Generic.Dictionary<string,string>()
dict.Add("a","hello")
let (e1success,e1) = dict.TryGetValue("a")
let (e2success,e2) = dict.TryGetValue("b")

let makeResource name = 
    { new System.IDisposable 
        with member this.Dispose() = printfn "%s disposed" name }

https://fsharpforfunandprofit.com/posts/completeness-seamless-dotnet-interop/

Summary

  • Referencing F# code from C# (FSharp.Core package)
  • Conforming to the F# Component Design Guidelines
  • Using .NET libraries from F#

Links

F# Web app

  • Add F# Console application project, "bowling.web" to the solution,
  • Add project reference from "bowling.web" to "bowling",
  • Open "paket.dependencies" file,
  • Add "Suave" package to main group:
 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
framework: net451
source https://www.nuget.org/api/v2

nuget FSharp.Core 4.0.0.1 redirects: force
nuget Suave

group Build
    source https://www.nuget.org/api/v2

    nuget FAKE
    nuget xunit.runner.console

group Tests
    framework: net451
    source https://www.nuget.org/api/v2

    nuget FSharp.Core 4.0.0.1 redirects: force
    nuget xUnit
    nuget FsUnit.xUnit

Add "New Item", "paket.references" to "bowling.web":

1: 
Suave

Run paket install:

1: 
> .paket\paket.exe install
  • Open "Program.fs" from "bowling.web",
  • Remove boilerplater code, and insert following hello world suave:
1: 
2: 
3: 
open Suave

startWebServer defaultConfig (Successful.OK "Hello world")

Run it!

hello_world_suave.png

Exercise

Implement scoreHandler function so that:

  • it responds with 200 OK with score for correct input,
  • it responds with 400 BAD REQUEST with "Wrong result" message for wrong input:
1: 
2: 
3: 
4: 
5: 
6: 
open Suave

let scoreHandler (input: string) : WebPart =
    Successful.OK "Hello world"

startWebServer defaultConfig (Filters.pathScan "/%s" scoreHandler)

Hint: Make use of Successful.OK and RequestErrors.BAD_REQUEST functions. Both are of type string -> WebPart.

Demo: .fs files order in project

(order matters)

At first it looks like a limitation but it really turns out to be one of the most beloved F# features

Summary

  • Suave.IO is a very light-weight server library, easy to use with F#
  • .fs file order inside project matters

Links

Summary

  • F# Library (bowling score)
  • F# Console app
  • F# Build script - FAKE
  • F# Test project - xUnit
  • C# Window app - WPF (integration with F#)
  • F# Web app