Skip to content

Instantly share code, notes, and snippets.

@dmitry-a-morozov
Last active December 14, 2019 16:57
Show Gist options
  • Save dmitry-a-morozov/2401778080f1d02bdba1ac921e8d5eb5 to your computer and use it in GitHub Desktop.
Save dmitry-a-morozov/2401778080f1d02bdba1ac921e8d5eb5 to your computer and use it in GitHub Desktop.

Assert On Steroids

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

image

or exception logged to console

image

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

image

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:

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 !

@davidglassborow
Copy link

Nice !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment