I find it useful to have assert
statements in my code. It has several benefits:
- Self documents code expectations
- Establishes boundary checks between component
- Helps to pinpoint problems by failing fast, especially when you just started dealing with a new library
- Here is a good summary on using assertions.
In a dream world all this can be addressed by sophisticated type system but we all know it’s not going to happen any time soon.
F# has built-in assert, which mapped to Debug.Assert
. Annoying thing is that it doesn’t display boolean expression that failed. Small test program
open System.Diagnostics
let x = "hello"
Debug.Assert(x.Length > 5)
Will fail depending on your configuration/framework with either meaningless message box
or exception logged to console
This is a sub-optimal experience usually improved by explicitly invoking Debug.Assert
overload that receives a text message as a second argument
Debug.Assert(x.Length > 5, "x.Length > 5")
This is acceptable solution but at the cost of mechanical repetition. We could do better in F# !
module FSharp.Diagnostics
open System.Diagnostics
open FSharp.Quotations
open System.Runtime.CompilerServices
open Patterns
open DerivedPatterns
open System
open FSharp.Linq.RuntimeHelpers
type Debug =
[<ConditionalAttribute("DEBUG")>]
static member Assert([<ReflectedDefinition>] condition: Expr<bool>,
[<CallerFilePath>]?filePath: string, [<CallerLineNumber>]?line: int) : unit =
let rec prettyPrinty e =
match e with
| SpecificCall <@ (=) @> (None, _, [ lhs; rhs ]) ->
sprintf "%s = %s" (prettyPrinty lhs) (prettyPrinty rhs)
| SpecificCall <@ (<) @> (None, _, [ lhs; rhs ]) ->
sprintf "%s < %s" (prettyPrinty lhs) (prettyPrinty rhs)
| SpecificCall <@ (>) @> (None, _, [ lhs; rhs ]) ->
sprintf "%s > %s" (prettyPrinty lhs) (prettyPrinty rhs)
| SpecificCall <@ (<>) @> (None, _, [ lhs; rhs ]) ->
sprintf "%s <> %s" (prettyPrinty lhs) (prettyPrinty rhs)
| SpecificCall <@ (>=) @> (None, _, [ lhs; rhs ]) ->
sprintf "%s >= %s" (prettyPrinty lhs) (prettyPrinty rhs)
| SpecificCall <@ (<=) @> (None, _, [ lhs; rhs ]) ->
sprintf "%s <= %s" (prettyPrinty lhs) (prettyPrinty rhs)
| SpecificCall <@ not @> (None, _, [ x ]) ->
sprintf "not %s" (prettyPrinty x)
| PropertyGet( Some( self), prop, args) ->
sprintf "%s.%s" (prettyPrinty self) prop.Name
| Call(None, method, xs) ->
let args = xs |> List.map prettyPrinty |> String.concat","
let invocation =
if Char.IsLower(method.Name.[0]) && xs.Length = 1
then sprintf "%s %s" method.Name (prettyPrinty xs.Head)
else sprintf "%s(%s)" method.Name args
sprintf "%s.%s" method.DeclaringType.Name invocation
| OrElse (lhs, rhs) ->
sprintf "%s || %s" (prettyPrinty lhs) (prettyPrinty rhs)
| AndAlso (lhs, rhs) ->
sprintf "%s && %s" (prettyPrinty lhs) (prettyPrinty rhs)
| ValueWithName(_, _, name) ->
name
| Value(x, _) ->
sprintf "%A" x
| _ -> ""
let evalCondition = LeafExpressionConverter.EvaluateQuotation(condition) :?> _
if not evalCondition
then
let expr = prettyPrinty condition
let location = sprintf "\nat %s:line %i" filePath.Value line.Value
let message = sprintf "Assertion (%s) failed%s" expr location
Debug.Fail(message)
Module FSharp.Diagnostics
can be used as a “drop-in” to magically “lighten up” Debug.Assert
. Once you include the module at the top of any file that uses partially qualified Debug.Assert
you’ll see full boolean condition that failed:
module Program
open System.Diagnostics
open FSharp.Diagnostics //shadow default Debug.Assert
[<EntryPoint>]
let main _ =
let x = "hello"
Debug.Assert(x.Length > 5)
0
Maestro Don Syme would not recommend this “drop-in” approach. But what does he know, right? :)
You can add [<AutoOpen>]
at the top of FSharp.Diagnostics
module if you feel adventurous.
Even if you’re skeptical about usefulness of assertions in your code the module is a nice demo of F# language features:
- Functional first
- First-class curried functions (including local definitions)
- Recursion
- Modules
- Pattern matching, active patterns
- Quotations
- Type inference
- NET interop
- Recent additions to F#
Both prettyPrint
and LeafExpressionConverter.EvaluateQuotation
are not equipped to deal with arbitrarily complex F# code. This is good because if you have complex expression as boolean condition for assert there is something wrong with your code. It’s possible that I missed some cases for prettyPrint. Feel free to extend it.
One can go as far as using same code with Trace.Assert
in production release build. Nothing wrong with that but make sure you understand how to configure Trace.Listeners
to fit production environment expectations. Something like
Trace.Listeners.Clear()
Trace.Listeners.Add {
new DefaultTraceListener(AssertUiEnabled = false) with
member __.Fail( message) = failwith message
} |> ignore
might do the job.
Happy holidays F# community !
Nice !