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// "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
|> helperCDiffs 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 CardInfolet 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 ->
...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
)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/
- 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.
- have
fsifile extension. - must be above the corresponding
fsfile.
| | 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"
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
- Avoid exposing F# types if possible
- NOTE: If
Fsharp.Core.dllis missing, use nuget to add package
- Expose API in a .NET friendly manner. Add a module called either
ApiorApi.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 asIEnumerable<int>in C# - F#
ResizeArray<int>-- same asList<int>in C# - F#
int[]-- same asArray<int>in C#
- F#
-
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
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)