Skip to content

Instantly share code, notes, and snippets.

@akhansari
Forked from swlaschin/effective-fsharp.md
Created October 28, 2019 17:11
Show Gist options
  • Select an option

  • Save akhansari/3189c8350061520d9eab132c602bb33a to your computer and use it in GitHub Desktop.

Select an option

Save akhansari/3189c8350061520d9eab132c602bb33a to your computer and use it in GitHub Desktop.
Effective F#, tips and tricks

General code layout

Reading happens much more than writing, so make it readble first. Make the INTENT understandable During a code review if reader and writer disagree, reader is always right!

Make blocks obvious style - have only one indent per scope change.

// good
let xxxx xx xx =
  xxxx xx xx
  xxx xx 
  xxxx xxx
  xxxx xxx
  xxxx xx xxxx

// badly indented blocks
let xxxx xx xx =
  xxxx xx xx
    xxx xx 
   xxxx xxx
        xxxx xxx
     xxxx xx xxxx

Function style

// "stanza" style
let xxx = 
  // part 1
  do this 
  and then this 
  and then something else 
  
  // part 2
  start a new thing 
  and some more

  // part 3
  ...

A long function is ok if everything belongs together -- don't split it up for the sake of it

// "recipe" style
let xxx = 
  // assemble ingredients
  let helperA = ....
  let helperB = ...
  let helperC = ...
  
  // then combine the ingredients
  start
  |> helperA
  |> helperB
  |> helperC

If you have "big recipe" when ingredients are more than one liners, then create a private helper module to put the code in.

module private Ingredients =
  // define ingredients
  let helperA = 
    ...
  let helperB = 
    ...
  let helperC = 
    ...

// public  
let xxx = 
  // combine the ingredients defined above
  start
  |> helperA
  |> helperB
  |> helperC

Layout for diffs

Diffs are an important part of reading so make diffs easy to understand. Changing one thing should cause only one line to change!

// 1 or 2 fields
type MyRecord = { fieldA:string; fieldB:int }

// 3 or more fields
type MyRecord = {  
   fieldA : string
   fieldB : string
   fieldC : string
   fieldD : string
   }

Use same style for constructing records

let myRecord = {  
   fieldA = "..."
   fieldB = "..."
   fieldC = "..."
   fieldD = "..."
   }
// enum-style choices
type Colour = Red | Blue | Green

// single case choices
type CustomerId = CustomerId of int

// complex choices   
type Payment = 
   | Cash
   | Check of CheckNumber
   | Card of CardInfo

Formatting match expressions

let xxx p = 
  match p with
  | CaseA z -> ...
  | CaseB z -> ...
  | CaseC z -> ...

For longer ones, put each handler in a new block

let xxx p = 
  match p with
  | CaseA z -> 
	...
  | CaseB z -> 
	...
  | CaseD z -> 
	...

Formatting inline lambdas

Short ones can go on one line

aCollection
|> List.map (fun x -> x + 1)

Longer ones may need to start a new block

aCollection
|> List.map (fun x -> 
   xxxxx xxxx
   xxxx xxx
   )

Do NOT have "hanging" lambdas. If the top line changes, the indentation of the entire block will change, breaking the "diff" rule above.

// example of BAD indenting. 
// If List.map is changed to List.choose, say, the entire block changes.
aCollection
|> List.map (fun x -> 
             xxxxx xxxx
             xxxx xxx
             )

OO style vs FP style

It's OK to use OO style code if behavior is the most important thing -- that is, you want polymorphism.

e.g. in x.ToString() we don't care what x is! See https://fsharpforfunandprofit.com/posts/type-extensions/

But this can mess with type inference :( You might well need to use type annotations more often.

Similarly, it's OK to use interfaces to define groups of functions that can have multiple implementations. See https://fsharpforfunandprofit.com/posts/interfaces/

Modules and namespaces

  • A namespace is just like C#
  • A "module" is like a C# class with only static methods

If defining types only (eg domain), use a namespace. If defining functions, they MUST be in a module (otherwise youe get a compiler error)

For functions closely associated with a type (eg create/value), use a module with the same name as the type.

NOTES:

  • You can't use namespaces in F# interactive
  • You can use "rec" for recursive modules so that the definitions don't have to be in order.

Signature files

  • have fsi file extension.
  • must be above the corresponding fs file.
|             | FSI              | FS          |
| top of file | namespace/module | same as FSI |
| types       | public           | public types must be defined the same |
|             |                  | private types OK |
| functions   | use "val"        | use "let"        |

Example signature file

/// Signature file
module CurrentAwareness.Dto.Converter

open CurrentAwareness

type DtoError =
    | BadName

module CuratedItem =
    val fromDomain : CuratedItem -> CuratedItemDto
    val toDomain : CuratedItemDto -> Result<CuratedItem,DtoError>

Example implementation file

module CurrentAwareness.Dto.Converter

open CurrentAwareness

type DtoError =
    | BadName

type MyPrivateType = string

module CuratedItem =
    let fromDomain (x:CuratedItem) :CuratedItemDto =
        failwith "not implemented"

    let toDomain (x:CuratedItemDto) :Result<CuratedItem,DtoError> =
        failwith "not implemented"

DTOs and Validation

Define them in their own module. For each DTO, define an associated module with functions "toDomain" and "fromDomain"

namespace CurrentAwareness.Dto

type MyDto = {
    Something: string
    }

/// could be in different module if you want to hide the implementation
module MyDto = 

   // may fail if DTO has bad data
   let toDomain (dto:MyDto) :Validation<MyDomain,ValidationError) =

   // always succeeds   
   let fromDomain (domainObj:MyDomain) :MyDto =

If using validation (multiple errors), the toDomain code has the same pattern

  • Define a ctor for the DTO
  • Create all the values (which return Results)
  • Use the applicative style to construct the DTO
  • Map the list of validation errors to an error case
let toDomainObj dto = 
  let ctor = ...
  let firstParam = ... dto.First
  let secondParam = ...
  let thirdParam = ...
  let domainObjR = ctor <!> firstParam <*> secondParam <*> thirdParam
  domainObjR |> Result.mapError ValidationError

See also:

If using validation WITHOUT multiple errors, the toDomain code can be simpler:

let toDomainObj dto = 
  result {
    let! firstParam = ... dto.First
    let! secondParam = ...
    let! thirdParam = ...
    domainObj = normal ctor 
    return domainObj
    }

See

Interacting with C#

  • Avoid exposing F# types if possible
  • NOTE: If Fsharp.Core.dll is missing, use nuget to add package

Exposing an API to C#

  • Expose API in a .NET friendly manner. Add a module called either Api or Api.Csharp
  • Understand tuple-style vs curried functions. Expose tuple-style only.
module Api =
  // correct
  let DoSomething(x,y,x) = 

  // incorrect
  let DoSomething x y x = 
  • Use C#-compatible collections not F# list

    • F# list -- not available in C#
    • F# int seq -- same as IEnumerable<int> in C#
    • F# ResizeArray<int> -- same as List<int> in C#
    • F# int[] -- same as Array<int> in C#
  • Have a CSharpHelper module with useful functions

module List =
    // IEnumerable to F# list
    let EnumToList enum = enum |> List.ofSeq
    // IEnumerable from F# list
    let EnumFromList list = list |> List.toSeq
  • Use Func<> instead of F# functions
  • NOTE: F# Async needs to be converted to C# Task
  • NOTE: F# float == C# double
  • NOTE: C# Enums are not the same as choice/union types
  • Don't expose tuples in Api

Exposing choice types to C#

Create a Match function for each choice type you expose, with a Func for each case. Here's the one for Result:

module Result =
    let Match(result, onOk:Func<_,_>, onError:Func<_,_>) =
        match result with
        | Ok x -> onOk.Invoke(x)
        | Error e -> onError.Invoke(e)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment