Opinionated Fable2 - Architecture & Performance

Explore Fable2's architecture improvements and performance gains. See how the new compiler generates smaller, optimized JavaScript with better tree shaking.

25 min read 4,880 words

Nothing can be more beautiful if you wish to come. And it seems Fable2 has made my wish true in most cases. Let's revisit my old blog and check what has improved and changed. So, the goal of this blog is it should be the same as the old blog, but it should be smaller. Because of the new and shiny Fable2.

Fable 2 is currently in beta but as per the blog, it is very much working. So, let's begin

2

Performance

Line of Code

Fable 2 has great improvements in code generation. Now, the generated code has a better tree shaking story and also less reflection. Let's have a look at old and new code...

F# Types

Let's start with simple F# types. What is there in Fable and what is not with Fable 2?

Let's take a simple example

type InfoModel = {
        FirstName : string
        LastName : string
        DOB : string
        Gender : string
        IsValid : bool
    }

PS: I'm using Fable REPL and Fable 2 REPL for testing out the code. It's a great way you can see what our F# code is doing. It will generate hell pretty JavaScript.

Let's see the generated JavaScript for Fable

import { setType } from "fable-core/Symbol";
import _Symbol from "fable-core/Symbol";
import { compareRecords, equalsRecords } from "fable-core/Util";
export class InfoModel {
  constructor(firstName, lastName, dOB, gender, isValid) {
    this.FirstName = firstName;
    this.LastName = lastName;
    this.DOB = dOB;
    this.Gender = gender;
    this.IsValid = isValid;
  }

  [_Symbol.reflection]() {
    return {
      type: "Test.InfoModel",
      interfaces: ["FSharpRecord", "System.IEquatable", "System.IComparable"],
      properties: {
        FirstName: "string",
        LastName: "string",
        DOB: "string",
        Gender: "string",
        IsValid: "boolean"
      }
    };
  }

  Equals(other) {
    return equalsRecords(this, other);
  }

  CompareTo(other) {
    return compareRecords(this, other) | 0;
  }

}
setType("Test.InfoModel", InfoModel);

And here is Fable 2

import { declare, Record } from "fable-core/Types";
export const InfoModel = declare(function InfoModel(arg1, arg2, arg3, arg4, arg5) {
  this.FirstName = arg1;
  this.LastName = arg2;
  this.DOB = arg3;
  this.Gender = arg4;
  this.IsValid = arg5;
}, Record);

Now that is crazy improvement.

Let's add some more code in there.

type InfoModel = {
        FirstName : string
        LastName : string
        DOB : string
        Gender : string
        IsValid : bool
    } with
        static member Empty = {
            FirstName = "Don"
            LastName = "Syme"
            DOB = "unknown"
            Gender = "male"
            IsValid = true
        }

A typical F# way of writing a Record Type or Model.

and here is the JavaScript code for Fable

import { setType } from "fable-core/Symbol";
import _Symbol from "fable-core/Symbol";
import { compareRecords, equalsRecords } from "fable-core/Util";
export class InfoModel {
  constructor(firstName, lastName, dOB, gender, isValid) {
    this.FirstName = firstName;
    this.LastName = lastName;
    this.DOB = dOB;
    this.Gender = gender;
    this.IsValid = isValid;
  }

  [_Symbol.reflection]() {
    return {
      type: "Test.InfoModel",
      interfaces: ["FSharpRecord", "System.IEquatable", "System.IComparable"],
      properties: {
        FirstName: "string",
        LastName: "string",
        DOB: "string",
        Gender: "string",
        IsValid: "boolean"
      }
    };
  }

  Equals(other) {
    return equalsRecords(this, other);
  }

  CompareTo(other) {
    return compareRecords(this, other) | 0;
  }

  static get Empty() {
    return new InfoModel("Don", "Syme", "unknown", "male", true);
  }

}
setType("Test.InfoModel", InfoModel);

And here is the code for Fable 2

import { declare, Record } from "fable-core/Types";
export const InfoModel = declare(function InfoModel(arg1, arg2, arg3, arg4, arg5) {
  this.FirstName = arg1;
  this.LastName = arg2;
  this.DOB = arg3;
  this.Gender = arg4;
  this.IsValid = arg5;
}, Record);
export function InfoModel$$$get_Empty() {
  return new InfoModel("Don", "Syme", "unknown", "male", true);
}

love

In Fable, we need to use something called [<Pojo>] attribute in code. It is removed in Fable 2. No more dancing around module and type.

In Fable, [<Pojo>] is used like this and generates the below JavaScript. That is default in Fable 2. A request I had in the old blog and very much granted in the latest Fable 2

open FSharp.Core
open Fable.Core

[<Pojo>]
type InfoModel = {
        FirstName : string
        LastName : string
        DOB : string
        Gender : string
        IsValid : bool
    }
    module InfoModel =
        let Empty = {
            FirstName = "Don"
            LastName = "Syme"
            DOB = "unknown"
            Gender = "male"
            IsValid = true
        }

In JavaScript, it looks like

export const InfoModelModule = function (__exports) {
  const Empty = __exports.Empty = {
    FirstName: "Don",
    LastName: "Syme",
    DOB: "unknown",
    Gender: "male",
    IsValid: true
  };
  return __exports;
}({});

Now let's see how it looks in Fable 2

type InfoModel = {
        FirstName : string
        LastName : string
        DOB : string
        Gender : string
        IsValid : bool
    } with static member Empty = {
            FirstName = "Don"
            LastName = "Syme"
            DOB = "unknown"
            Gender = "male"
            IsValid = true
        }

And here is the generated code

import { declare, Record } from "fable-core/Types";
export const InfoModel = declare(function InfoModel(arg1, arg2, arg3, arg4, arg5) {
  this.FirstName = arg1;
  this.LastName = arg2;
  this.DOB = arg3;
  this.Gender = arg4;
  this.IsValid = arg5;
}, Record);
export function InfoModel$$$get_Empty() {
  return new InfoModel("Don", "Syme", "unknown", "male", true);
}

No pojo and no modules. Pure types and still get the same result as above. Can't ask for more.

Lenses and Spectacles

The above InfoModel is perfect to represent a view. But what about validation? It is not holding any property to hold domain details. So, let's write that and see the generated code.

open FSharp.Core
open Fable.Core

[<Pojo>]
type Validate = {
    IsValid : bool
    ErrMsg : string
}

    module Validate =
        let Success = {
            IsValid = true
            ErrMsg = ""
        }
        let Failure (msg : string) = {
            IsValid = false
            ErrMsg = msg
        }

        let InitialValidate = {
            IsValid = true
            ErrMsg = ""
        }

[<Pojo>]
type InfoModel = {
        FirstName : string
        LastName : string
        DOB : string
        Gender : string
        IsValid : bool
    }
    module InfoModel =
        let Empty = {
            FirstName = "Don"
            LastName = "Syme"
            DOB = "unknown"
            Gender = "male"
            IsValid = true
        }

[<Pojo>]
type InfoErrorModel = {
    FirstName : Validate
    LastName : Validate
    DOB : Validate
    Gender : Validate
}
    module InfoErrorModel =
        let Empty = {
            FirstName = Validate.InitialValidate
            LastName = Validate.InitialValidate
            DOB = Validate.InitialValidate
            Gender = Validate.InitialValidate
        }

[<Pojo>]
type Model = {
    InfoModel : InfoModel
    InfoErrorModel : InfoErrorModel
}
    module Model =
        let Empty = {
            InfoModel = InfoModel.Empty
            InfoErrorModel = InfoErrorModel.Empty
        }

As we can see, it is a very typical kind of domain model code. Typical F# - DDD 101 stuff. I will explain domain models in a separate section, but let's first see the generated JavaScript code.

export const ValidateModule = function (__exports) {
  const Success = __exports.Success = {
    IsValid: true,
    ErrMsg: ""
  };

  const Failure = __exports.Failure = function (msg) {
    return {
      IsValid: false,
      ErrMsg: msg
    };
  };

  const InitialValidate = __exports.InitialValidate = {
    IsValid: true,
    ErrMsg: ""
  };
  return __exports;
}({});
export const InfoModelModule = function (__exports) {
  const Empty = __exports.Empty = {
    FirstName: "Don",
    LastName: "Syme",
    DOB: "unknown",
    Gender: "male",
    IsValid: true
  };
  return __exports;
}({});
export const InfoErrorModelModule = function (__exports) {
  const Empty_1 = __exports.Empty = {
    FirstName: ValidateModule.InitialValidate,
    LastName: ValidateModule.InitialValidate,
    DOB: ValidateModule.InitialValidate,
    Gender: ValidateModule.InitialValidate
  };
  return __exports;
}({});
export const ModelModule = function (__exports) {
  const Empty_2 = __exports.Empty = {
    InfoModel: InfoModelModule.Empty,
    InfoErrorModel: InfoErrorModelModule.Empty
  };
  return __exports;
}({});

Let's see how Fable 2 will improve the F# and JavaScript code.

type Validate = {
    IsValid : bool
    ErrMsg : string
}
  with 
    static member Success = {
        IsValid = true
        ErrMsg = ""
    }
    static member Failure (msg : string) = {
        IsValid = false
        ErrMsg = msg
    }

    static member InitialValidate = {
        IsValid = true
        ErrMsg = ""
    }

type InfoModel = {
        FirstName : string
        LastName : string
        DOB : string
        Gender : string
        IsValid : bool
    }
    with static member Empty = {
            FirstName = "Don"
            LastName = "Syme"
            DOB = "unknown"
            Gender = "male"
            IsValid = true
        }

type InfoErrorModel = {
    FirstName : Validate
    LastName : Validate
    DOB : Validate
    Gender : Validate
}
    with static member Empty = {
            FirstName = Validate.InitialValidate
            LastName = Validate.InitialValidate
            DOB = Validate.InitialValidate
            Gender = Validate.InitialValidate
        }

type Model = {
    InfoModel : InfoModel
    InfoErrorModel : InfoErrorModel
}
    with static member Empty = {
            InfoModel = InfoModel.Empty
            InfoErrorModel = InfoErrorModel.Empty
        }

and the generated JavaScript is

import { declare, Record } from "fable-core/Types";
export const Validate = declare(function Validate(arg1, arg2) {
  this.IsValid = arg1;
  this.ErrMsg = arg2;
}, Record);
export function Validate$$$get_Success() {
  return new Validate(true, "");
}
export function Validate$$$Failure$$Z721C83C5(msg) {
  return new Validate(false, msg);
}
export function Validate$$$get_InitialValidate() {
  return new Validate(true, "");
}
export const InfoModel = declare(function InfoModel(arg1, arg2, arg3, arg4, arg5) {
  this.FirstName = arg1;
  this.LastName = arg2;
  this.DOB = arg3;
  this.Gender = arg4;
  this.IsValid = arg5;
}, Record);
export function InfoModel$$$get_Empty() {
  return new InfoModel("Don", "Syme", "unknown", "male", true);
}
export const InfoErrorModel = declare(function InfoErrorModel(arg1, arg2, arg3, arg4) {
  this.FirstName = arg1;
  this.LastName = arg2;
  this.DOB = arg3;
  this.Gender = arg4;
}, Record);
export function InfoErrorModel$$$get_Empty() {
  return new InfoErrorModel(Validate$$$get_InitialValidate(), Validate$$$get_InitialValidate(), Validate$$$get_InitialValidate(), Validate$$$get_InitialValidate());
}
export const Model = declare(function Model(arg1, arg2) {
  this.InfoModel = arg1;
  this.InfoErrorModel = arg2;
}, Record);
export function Model$$$get_Empty() {
  return new Model(InfoModel$$$get_Empty(), InfoErrorModel$$$get_Empty());
}

The generated JavaScript is a little less readable but the code is more concise. As long as everything is in English, I am okay with that.

I still hold the opinion of avoiding aether here. If you need that, then there is a need to revisit what you are doing. Specifically for the Elmish part. Else, lenses are very much okay in most of the server side code. But still use them with caution.

Domain Domain Domain

Just like everyone in the Functional Domain, I am a big fan of Domain-Driven Design and Scott W. It makes things easier to represent. What is the use of Fable if I can't use it in the front end? And Fable did generate way too much JavaScript code. Let's see how Fable 2 does in this case.

Simple string testing

type Name private(s : string) =
    member __.Name = s
    static member Create(s: string) =
        if (s <> "" || s <> null) then Ok s
        else Error "Invalid String"

converts to

import { setType } from "fable-core/Symbol";
import _Symbol from "fable-core/Symbol";
import Result from "fable-core/Result";
export class Name {
  [_Symbol.reflection]() {
    return {
      type: "Test.Name",
      properties: {
        Name: "string"
      }
    };
  }

  constructor(s) {
    this.s = s;
  }

  get Name() {
    return this.s;
  }

  static Create(s) {
    if (s !== "" ? true : s != null) {
      return new Result(0, s);
    } else {
      return new Result(1, "Invalid String");
    }
  }

}
setType("Test.Name", Name);

In case of Fable 2

import { Result } from "fable-core/Option";
import { declare } from "fable-core/Types";
export const Name = declare(function Name(s$$1) {
  const $this$$1 = this;
  $this$$1.s = s$$1;
});

function Name$$$$002Ector$$Z721C83C5(s$$1) {
  return this != null ? Name.call(this, s$$1) : new Name(s$$1);
}

export function Name$$get_Name(__) {
  return __.s;
}
export function Name$$$Create$$Z721C83C5(s) {
  if (s !== "" ? true : s !== null) {
    return new Result(0, "Ok", s);
  } else {
    return new Result(1, "Error", "Invalid String");
  }
}

No words

Let's try it another way

type Name = private | Name of string
    with
    member this.String = let (Name s) = this in s
    static member Create(s : string)=
        if (s <> "" || s <> null ) then Ok s
        else Error "Invalid string"

converts to

import { setType } from "fable-core/Symbol";
import _Symbol from "fable-core/Symbol";
import { compareUnions, equals } from "fable-core/Util";
import Result from "fable-core/Result";

class Name {
  constructor(tag, data) {
    this.tag = tag | 0;
    this.data = data;
  }

  [_Symbol.reflection]() {
    return {
      type: "Test.Name",
      interfaces: ["FSharpUnion", "System.IEquatable", "System.IComparable"],
      cases: [["Name", "string"]]
    };
  }

  Equals(other) {
    return this === other || this.tag === other.tag && equals(this.data, other.data);
  }

  CompareTo(other) {
    return compareUnions(this, other) | 0;
  }

  get String() {
    return this.data;
  }

  static Create(s) {
    if (s !== "" ? true : s != null) {
      return new Result(0, s);
    } else {
      return new Result(1, "Invalid string");
    }
  }

}

setType("Test.Name", Name);

And in case of Fable 2

import { Result } from "fable-core/Option";
import { declare, Union } from "fable-core/Types";
export const Name = declare(function Name(tag, name, ...fields) {
  Union.call(this, tag, name, ...fields);
}, Union);
export function Name$$get_String(this$) {
  const s$$1 = this$.fields[0];
  return s$$1;
}
export function Name$$$Create$$Z721C83C5(s) {
  if (s !== "" ? true : s !== null) {
    return new Result(0, "Ok", s);
  } else {
    return new Result(1, "Error", "Invalid string");
  }
}

claps

There is a detailed explanation in my old blog about how DDD is different in the case of server and client. That holds true even now, so you can always check that out.

London Rail

How is ROP going to stand in case of Fable 2? Does that improve also?

Here is code directly copy-pasted from the site with a simple example

open FSharp.Core
open Fable.Core
open System

// convert a single value into a two-track result
let succeed x =
    Ok x

// convert a single value into a two-track result
let fail (x) =
    Error x

// apply either a success function or failure function
let either successFunc failureFunc twoTrackInput =
    match twoTrackInput with
    | Ok s -> successFunc s
    | Error f -> failureFunc f

// convert a switch function into a two-track function
let bind f =
    either f fail

// pipe a two-track value into a switch function
let (>>=) x f =
    bind f x

// compose two switches into another switch
let (>=>) s1 s2 =
    s1 >> bind s2

// convert a one-track function into a switch
let switch f =
    f >> succeed

// convert a one-track function into a two-track function
let map f =
    either (f >> succeed) fail

// convert a dead-end function into a one-track function
let tee f x =
    f x; x

// convert a one-track function into a switch with exception handling
let tryCatch f exnHandler x =
    try
        f x |> succeed
    with
    | ex -> exnHandler ex |> fail

// convert two one-track functions into a two-track function
let doubleMap successFunc failureFunc =
    either (successFunc >> succeed) (failureFunc >> fail)

// add two switches in parallel
let plus addSuccess addFailure switch1 switch2 x =
    match (switch1 x),(switch2 x) with
    | Ok s1, Ok s2 -> Ok (addSuccess s1 s2)
    | Error f1, Ok _  -> Error f1
    | Ok _ ,Error f2 -> Error f2
    | Error f1,Error f2 -> Error (addFailure f1 f2)

let collect errorFn xs =
    xs |> Seq.fold (fun res next ->
                        match res, next with
                        | Ok r, Ok i -> Ok (i::r)
                        | Ok _, Error m | Error m, Ok _ -> Error m
                        | Error m, Error n -> Error (errorFn m n)
    ) (Ok []) |> map List.rev

let validation1 (s:string) = if (String.IsNullOrEmpty(s)) then Error "String should not be empty" else Ok s

let validation2 (s:string) = if (s.Length > 50) then Error "Can't be more than 50" else Ok s

let combinedValidation = validation1 >> bind validation2

And here is the JavaScript code.

import Result from "fable-core/Result";
import CurriedLambda from "fable-core/CurriedLambda";
import { reverse } from "fable-core/List";
import List from "fable-core/List";
import { fold } from "fable-core/Seq";
import { isNullOrEmpty } from "fable-core/String";
export function succeed(x) {
  return new Result(0, x);
}
export function fail(x) {
  return new Result(1, x);
}
export function either(successFunc, failureFunc, twoTrackInput) {
  if (twoTrackInput.tag === 1) {
    return failureFunc(twoTrackInput.data);
  } else {
    return successFunc(twoTrackInput.data);
  }
}
export function bind(f) {
  var failureFunc;
  return CurriedLambda((failureFunc = function (x) {
    return fail(x);
  }, function (twoTrackInput) {
    return either(f, failureFunc, twoTrackInput);
  }));
}
export function op_GreaterGreaterEquals(x, f) {
  return bind(f)(x);
}
export function op_GreaterEqualsGreater(s1, s2) {
  return CurriedLambda($var1 => bind(s2)(s1($var1)));
}

function _switch(f) {
  return CurriedLambda($var2 => function (x) {
    return succeed(x);
  }(f($var2)));
}

export { _switch as switch };
export function map(f) {
  var successFunc;
  var failureFunc;
  return CurriedLambda((successFunc = $var3 => function (x) {
    return succeed(x);
  }(f($var3)), failureFunc = function (x_1) {
    return fail(x_1);
  }, function (twoTrackInput) {
    return either(successFunc, failureFunc, twoTrackInput);
  }));
}
export function tee(f, x) {
  f(x);
  return x;
}
export function tryCatch(f, exnHandler, x) {
  try {
    return succeed(f(x));
  } catch (ex) {
    return fail(exnHandler(ex));
  }
}
export function doubleMap(successFunc, failureFunc) {
  var successFunc_1;
  var failureFunc_1;
  return CurriedLambda((successFunc_1 = $var4 => function (x) {
    return succeed(x);
  }(successFunc($var4)), failureFunc_1 = $var5 => function (x_1) {
    return fail(x_1);
  }(failureFunc($var5)), function (twoTrackInput) {
    return either(successFunc_1, failureFunc_1, twoTrackInput);
  }));
}
export function plus(addSuccess, addFailure, switch1, switch2, x) {
  const matchValue = [switch1(x), switch2(x)];

  if (matchValue[0].tag === 1) {
    if (matchValue[1].tag === 1) {
      return new Result(1, addFailure(matchValue[0].data, matchValue[1].data));
    } else {
      return new Result(1, matchValue[0].data);
    }
  } else if (matchValue[1].tag === 1) {
    return new Result(1, matchValue[1].data);
  } else {
    return new Result(0, addSuccess(matchValue[0].data, matchValue[1].data));
  }
}
export function collect(errorFn, xs) {
  return map(function (list) {
    return reverse(list);
  })(fold(function (res, next) {
    const matchValue = [res, next];
    const $var6 = matchValue[0].tag === 1 ? matchValue[1].tag === 1 ? [2, matchValue[0].data, matchValue[1].data] : [1, matchValue[0].data] : matchValue[1].tag === 1 ? [1, matchValue[1].data] : [0, matchValue[1].data, matchValue[0].data];

    switch ($var6[0]) {
      case 0:
        return new Result(0, new List($var6[1], $var6[2]));

      case 1:
        return new Result(1, $var6[1]);

      case 2:
        return new Result(1, errorFn($var6[1], $var6[2]));
    }
  }, new Result(0, new List()), xs));
}
export function validation1(s) {
  if (isNullOrEmpty(s)) {
    return new Result(1, "String should not be empty");
  } else {
    return new Result(0, s);
  }
}
export function validation2(s) {
  if (s.length > 50) {
    return new Result(1, "Can't be more than 50");
  } else {
    return new Result(0, s);
  }
}
export const combinedValidation = CurriedLambda($var7 => bind(function (s_1) {
  return validation2(s_1);
})(function (s) {
  return validation1(s);
}($var7)));

and here is the case for Fable 2

import { reverse } from "fable-core/List";
import { Result } from "fable-core/Option";
import { fold } from "fable-core/Seq";
import { isNullOrEmpty } from "fable-core/String";
import { L } from "fable-core/Types";
export function succeed(x$$13) {
  return new Result(0, "Ok", x$$13);
}
export function fail(x$$12) {
  return new Result(1, "Error", x$$12);
}
export function either(successFunc$$3, failureFunc$$4, twoTrackInput$$3) {
  if (twoTrackInput$$3.tag === 1) {
    const f$$6 = twoTrackInput$$3.fields[0];
    return failureFunc$$4(f$$6);
  } else {
    const s$$4 = twoTrackInput$$3.fields[0];
    return successFunc$$3(s$$4);
  }
}
export function bind(f$$5) {
  return function (twoTrackInput$$2) {
    return either(f$$5, fail, twoTrackInput$$2);
  };
}
export function op_GreaterGreaterEquals(x$$10, f$$4) {
  return bind(f$$4)(x$$10);
}
export function op_GreaterEqualsGreater(s1$$1, s2$$1) {
  return function ($arg$$6) {
    return bind(s2$$1)(s1$$1($arg$$6));
  };
}
export function switch$(f$$3) {
  return function ($arg$$5) {
    return succeed(f$$3($arg$$5));
  };
}
export function map(f$$2) {
  return function (twoTrackInput$$1) {
    return either(function successFunc$$2($arg$$4) {
      return succeed(f$$2($arg$$4));
    }, fail, twoTrackInput$$1);
  };
}
export function tee(f$$1, x$$6) {
  f$$1(x$$6);
  return x$$6;
}
export function tryCatch(f, exnHandler, x$$3) {
  try {
    return succeed(f(x$$3));
  } catch (ex) {
    return fail(exnHandler(ex));
  }
}
export function doubleMap(successFunc, failureFunc) {
  return function (twoTrackInput) {
    return either(function successFunc$$1($arg$$3) {
      return succeed(successFunc($arg$$3));
    }, function failureFunc$$1($arg$$2) {
      return fail(failureFunc($arg$$2));
    }, twoTrackInput);
  };
}
export function plus(addSuccess, addFailure, switch1, switch2, x) {
  const matchValue$$1 = [switch1(x), switch2(x)];

  if (matchValue$$1[0].tag === 1) {
    if (matchValue$$1[1].tag === 1) {
      return new Result(1, "Error", addFailure(matchValue$$1[0].fields[0], matchValue$$1[1].fields[0]));
    } else {
      return new Result(1, "Error", matchValue$$1[0].fields[0]);
    }
  } else if (matchValue$$1[1].tag === 1) {
    return new Result(1, "Error", matchValue$$1[1].fields[0]);
  } else {
    return new Result(0, "Ok", addSuccess(matchValue$$1[0].fields[0], matchValue$$1[1].fields[0]));
  }
}
export function collect(errorFn, xs) {
  return map(reverse)(fold(function folder(res, next) {
    const matchValue = [res, next];
    var $target$$9, i, r, m, m$$1, n;

    if (matchValue[0].tag === 1) {
      if (matchValue[1].tag === 1) {
        $target$$9 = 2;
        m$$1 = matchValue[0].fields[0];
        n = matchValue[1].fields[0];
      } else {
        $target$$9 = 1;
        m = matchValue[0].fields[0];
      }
    } else if (matchValue[1].tag === 1) {
      $target$$9 = 1;
      m = matchValue[1].fields[0];
    } else {
      $target$$9 = 0;
      i = matchValue[1].fields[0];
      r = matchValue[0].fields[0];
    }

    switch ($target$$9) {
      case 0:
        {
          return new Result(0, "Ok", L(i, r));
          break;
        }

      case 1:
        {
          return new Result(1, "Error", m);
          break;
        }

      case 2:
        {
          return new Result(1, "Error", errorFn(m$$1, n));
          break;
        }
    }
  }, new Result(0, "Ok", L()), xs));
}
export function validation1(s$$3) {
  if (isNullOrEmpty(s$$3)) {
    return new Result(1, "Error", "String should not be empty");
  } else {
    return new Result(0, "Ok", s$$3);
  }
}
export function validation2(s$$2) {
  if (s$$2.length > 50) {
    return new Result(1, "Error", "Can't be more than 50");
  } else {
    return new Result(0, "Ok", s$$2);
  }
}
export function combinedValidation($arg$$1) {
  return bind(validation2)(validation1($arg$$1));
}

Similar code. I guess that is the reason that having simple pure functions is always a good thing for the application. So, not much of a visible improvement in this part.

Slice and Dice

Fable 2 is not much good without Elmish. Let's give it a shot with an Elmish example.

This is Fable code.

[<Pojo>]
type Validate = {
    IsValid : bool
    ErrMsg : string
}

module Validate =
    let Success = {
        IsValid = true
        ErrMsg = ""
    }
    let Failure (msg : string) = {
        IsValid = false
        ErrMsg = msg
    }

    let InitialValidate = {
        IsValid = true
        ErrMsg = ""
    }

[<Pojo>]
type InfoModel = {
    FirstName : string
    FirstNameErr : Validate
    LastName : string
    LastNameErr : Validate
    DOB : string
    DOBErr : Validate
    Gender : string
    GenderErr : Validate
    IsValid : bool
}

[<Pojo>]
type ContactModel = {
    Mobile : string
    MobileErr : Validate
    Home : string
    HomeErr : Validate
    Office : string
    OfficeErr : Validate
    Email : string
    EmailErr : Validate
    Email2 : string
    Email2Err : Validate
    IsValid : bool
}

[<Pojo>]
type Model = {
    Info : InfoModel
    Contact : ContactModel
}

In Fable 2, it would be written like this

type Validate = {
    IsValid : bool
    ErrMsg : string
} with

    static member Success = {
        IsValid = true
        ErrMsg = ""
    }
    static member Failure (msg : string) = {
        IsValid = false
        ErrMsg = msg
    }

    static member InitialValidate = {
        IsValid = true
        ErrMsg = ""
    }

type InfoModel = {
    FirstName : string
    FirstNameErr : Validate
    LastName : string
    LastNameErr : Validate
    DOB : string
    DOBErr : Validate
    Gender : string
    GenderErr : Validate
    IsValid : bool
}

type ContactModel = {
    Mobile : string
    MobileErr : Validate
    Home : string
    HomeErr : Validate
    Office : string
    OfficeErr : Validate
    Email : string
    EmailErr : Validate
    Email2 : string
    Email2Err : Validate
    IsValid : bool
}

type Model = {
    Info : InfoModel
    Contact : ContactModel
}

Here, consider we have two pages Info and Contact. And then we have an update method to update them. Pretty straightforward.

Until we have around 20 pages or 50 components on the same page

For the first time, I felt what slowness is while typing in a simple HTML input box. Things just dragged like we are typing in a WPF application on a Windows 98 machine.

Let's understand what is happening here. The model is holding information required for Views. Now, even though the Info view is active, it is having information about Contact also. This makes the model quite big. Specifically if there are three or four levels of deep models. Deep models make things easy to understand but quite difficult to update. The deeper and bigger the model is, the slower the update of property is. Now, in ELM / Elmish architecture, we rely on the model extensively for what we can see on the view. Even simple typing into an input requires us to update the model. And then there is validation and other stuff going on.

It also presents another weird problem. When we start the application, there is a need to initialize the models. So, for a big application, one needs to set all models at the start. To do that, we need to fire up too many requests to the server. For a sec, we consider that would be OK in the era of cloud, but it can't resolve the issue of dependent types. If some property is dependent on another, then we can't have them at the start, and we can't initialize the model.

No model, No view.

What is the alternative to this fat model?

Slim Union Type - Here is the above code written differently using Fable

open FSharp.Core
open Fable.Core
open System

[<Pojo>]
type Validate = {
    IsValid : bool
    ErrMsg : string
}

module Validate =
    let Success = {
        IsValid = true
        ErrMsg = ""
    }
    let Failure (msg : string) = {
        IsValid = false
        ErrMsg = msg
    }

    let InitialValidate = {
        IsValid = true
        ErrMsg = ""
    }

[<Pojo>]
type InfoModel = {
    FirstName : string
    FirstNameErr : Validate
    LastName : string
    LastNameErr : Validate
    DOB : string
    DOBErr : Validate
    Gender : string
    GenderErr : Validate
    IsValid : bool
}

[<Pojo>]
type ContactModel = {
    Mobile : string
    MobileErr : Validate
    Home : string
    HomeErr : Validate
    Office : string
    OfficeErr : Validate
    Email : string
    EmailErr : Validate
    Email2 : string
    Email2Err : Validate
    IsValid : bool
}

type Page = Info = 0  | Contact = 1

type PageModel = Info of InfoModel | Contact of ContactModel

[<Pojo>]
type Model = {
    Page : Page
    PageModel : PageModel
}

Here is the generated code for the second option

import { setType } from "fable-core/Symbol";
import _Symbol from "fable-core/Symbol";
import { compareUnions, equals, Any } from "fable-core/Util";
export const ValidateModule = function (__exports) {
  const Success = __exports.Success = {
    IsValid: true,
    ErrMsg: ""
  };

  const Failure = __exports.Failure = function (msg) {
    return {
      IsValid: false,
      ErrMsg: msg
    };
  };

  const InitialValidate = __exports.InitialValidate = {
    IsValid: true,
    ErrMsg: ""
  };
  return __exports;
}({});
export class PageModel {
  constructor(tag, data) {
    this.tag = tag | 0;
    this.data = data;
  }

  [_Symbol.reflection]() {
    return {
      type: "Test.PageModel",
      interfaces: ["FSharpUnion", "System.IEquatable", "System.IComparable"],
      cases: [["Info", Any], ["Contact", Any]]
    };
  }

  Equals(other) {
    return this === other || this.tag === other.tag && equals(this.data, other.data);
  }

  CompareTo(other) {
    return compareUnions(this, other) | 0;
  }

}
setType("Test.PageModel", PageModel);

Let's check the Fable 2 option

open FSharp.Core
open Fable.Core
open System

type Validate = {
    IsValid : bool
    ErrMsg : string
}with
    static member Success = {
        IsValid = true
        ErrMsg = ""
    }
    static member Failure (msg : string) = {
        IsValid = false
        ErrMsg = msg
    }

    static member InitialValidate = {
        IsValid = true
        ErrMsg = ""
    }

type InfoModel = {
    FirstName : string
    FirstNameErr : Validate
    LastName : string
    LastNameErr : Validate
    DOB : string
    DOBErr : Validate
    Gender : string
    GenderErr : Validate
    IsValid : bool
}

type ContactModel = {
    Mobile : string
    MobileErr : Validate
    Home : string
    HomeErr : Validate
    Office : string
    OfficeErr : Validate
    Email : string
    EmailErr : Validate
    Email2 : string
    Email2Err : Validate
    IsValid : bool
}

type Page = Info | Contact

type PageModel = Info of InfoModel | Contact of ContactModel

type Model = {
    Page : Page
    PageModel : PageModel
}

and here is the generated JavaScript

import { Union, declare, Record } from "fable-core/Types";
export const Validate = declare(function Validate(arg1, arg2) {
  this.IsValid = arg1;
  this.ErrMsg = arg2;
}, Record);
export function Validate$$$get_Success() {
  return new Validate(true, "");
}
export function Validate$$$Failure$$Z721C83C5(msg) {
  return new Validate(false, msg);
}
export function Validate$$$get_InitialValidate() {
  return new Validate(true, "");
}
export const InfoModel = declare(function InfoModel(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) {
  this.FirstName = arg1;
  this.FirstNameErr = arg2;
  this.LastName = arg3;
  this.LastNameErr = arg4;
  this.DOB = arg5;
  this.DOBErr = arg6;
  this.Gender = arg7;
  this.GenderErr = arg8;
  this.IsValid = arg9;
}, Record);
export const ContactModel = declare(function ContactModel(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11) {
  this.Mobile = arg1;
  this.MobileErr = arg2;
  this.Home = arg3;
  this.HomeErr = arg4;
  this.Office = arg5;
  this.OfficeErr = arg6;
  this.Email = arg7;
  this.EmailErr = arg8;
  this.Email2 = arg9;
  this.Email2Err = arg10;
  this.IsValid = arg11;
}, Record);
export const Page = declare(function Page(tag, name, ...fields) {
  Union.call(this, tag, name, ...fields);
}, Union);
export const PageModel = declare(function PageModel(tag, name, ...fields) {
  Union.call(this, tag, name, ...fields);
}, Union);
export const Model = declare(function Model(arg1, arg2) {
  this.Page = arg1;
  this.PageModel = arg2;
}, Record);

Here, you can see, I don't have to define Union as Enum just to get optimized JavaScript.

Want to save a few more lines?

Try this

type [<StringEnum>]Page = Info | Contact

The Page part will be removed from the generated code. As it will directly compare strings when and if required.

Old Habits Die Hard

JS interop is working as it was working earlier. And so does my personal favorite, Fulma.

Shameless Plug

sign me up

After working with many big and small consulting companies on various projects ranging from dotnet web to Node.js, single page applications to cross-platform mobile applications, I decided to go solo. There were hiccups at the start, but I did survive for almost a year. Not only did I survive, but I also started my own consulting company Fuzzy Cloud.

So, I am looking for new assignments. If anyone is interested in work or training with me, please contact me. I know we are so small compared to other friends from Europe and the USA, but we still share the same love for F# and functional programming in general. And I am always ready for challenging assignments.

I'd love to have feedback about this article and will update it if there are any additions or changes required.

Frequently Asked Questions

What are the main performance improvements in Fable 2?

Fable 2 significantly reduces generated JavaScript code size through better tree shaking and less reflection overhead. For example, a simple F# record type that generated over 40 lines of JavaScript in Fable now compiles to just 5-6 lines in Fable 2, making it a dramatic improvement in code efficiency.

How does Fable 2 handle F# record types differently?

Fable 2 uses a new `declare` function from `fable-core/Types` to generate cleaner record type definitions without the reflection metadata and utility function imports required in the original Fable. This results in more readable and compact JavaScript output.

Is Fable 2 ready for production use?

While Fable 2 is currently in beta, it is very much working and usable according to the official blog. The improvements in code generation and performance make it a solid choice for F# to JavaScript compilation projects.

Can I test Fable 2 code changes before committing?

Yes, you can use the Fable 2 REPL (available at fable.io/repl2/) to test your F# code and see the generated JavaScript output in real-time. This is a great way to understand how your F# code compiles and compare it with previous versions.

Share this article