December 12, 2021

F#LASHCARDS

Learn F#, and a language by implementing a flash-card program, step-by-step.

F#LASHCARDS

This is actually my first F# program, so I'll be learning along too! I'm not going to explain everything, but some interesting things as we go, and I'm going to be making mistakes.

Firstly, go and create a console app. I'm using Visual Studio 2019 as my editor and for the default .NET 5.0 template.

This is what my VS F# Console App template gave me inside a file called Program.fs:

// Learn more about F# at http://fsharp.org

open System

// Define a function to construct a message to print
let from whom =
    sprintf "from %s" whom

[<EntryPoint>]
let main argv =
    let message = from "F#" //Call the function
    printfn "Hello World %s" message
    0 // return an integer exit code

Pretty straightforward, but let's go over what we have.
We have a "from" function, which takes a parameter "whom". It's purpose seems to be to format the string "from %s" and replace "%s" with the string passed as whom.  Then we have a main function which defines a message by calling "from" function and then printing out Hello World and then the message. Finally it has an exit code. I think that by convention (and principle), the last line must be the return type?

In F#, you know something is a function because it's definition will have following parts:

  1. let keyword
  2. a name
  3. one or more arguments separated by spaces
  4. =, followed by newline (optional) and a function body.

First weirdness.

When I heard that a function has one or more arguments, my first immediate thought was - "hang on, what about a function with no arguments?". You can't do that in F#.

By convention when this comes up, you can use the Unit as an argument. Research tells me that the Unit type is LITERALLY a type to indicate the absence of a value to satisfy syntax. If the from function took the unit argument, it might look like this:

let from () = "from F#"

I thought initially this was a bit of a design flaw, but it does have a cool little consequence over using something like "void". While you would normally call a function like this:

let result = myFunctionWithParams param

For functions with no arguments (or the unit argument) it is called like this:

which looks pretty familiar!

let fromFsharpString = from ()

Also, it is necessary to make this distinction as otherwise result in below line would be the function itself, not the result of the function:

let fromFshapStringFunction = from

We are going to start by defining a "Card" type. A Card has two "Sides". One Side is "English" and the other side is a "Latin". "English" and "Latin" are both types of Sides. In OO (Object Oriented) land, we might say they're sub-classes, but I heard that Functional programming has no inheritance in it as a rule. Let's go ahead and define the general idea of a Side first after the main function - It is a simple "record" type with a "text" property.

// Learn more about F# at http://docs.microsoft.com/dotnet/fsharp

open System

// Define a function to construct a message to print
let from () = "from %s"

[<EntryPoint>]
let main argv =
    let message = from "F#" // Call the function
    printfn "Hello world %s" message
    0 // return an integer exit code

type Side = { text: string }

Oh, we can't. Try to compile, and it breaks. The EntryPoint function must be the last to be defined in the last file to be compiled. So... let's make a new file for our definitions.

I'm calling mine CardTypes.fs, moving Side definition to that.

module CardTypes

type Side = { text:string }

And now my Program.fs, I'll just add a reference to CardTypes by adding:

open System
open CardTypes //added this

The first mistake - how NOT to define a type

Let's keep going by adding the Language types. Now - I could say language is a property of a Side, and that would be valid - but I have a feeling that we may want different properties based on what language we're looking at later on, so I'm defining them as types for now:

module CardTypes

type Side = { text: string }
type English = Side
type Latin = Side

The syntax seems logical, if a bit different.

Now, let's describe a Card. It is made of two sides - one English and one Latin:

type Card = English * Latin

Let's test out that type.

To Create a Side, we define the data matching the structure of the type. F# makes a judgement call and infers that I created data matching the type.

let mySide = {text="hello"}

Now - in actuality, we know this is English. If we needed to specify this, we can:

let mySide = {text="hello"}:English

To create a Card, we just have to declare the two sides together.

let myCard = ({text="hello"} , {text="salve"})

Or do we? In fact, taking a closer look, what we have created is a "Side*Side" tuple. We need to explicitly state the classes while there is no way to distinguish between them.

let myCard = (({text="hello"}:English) , ({text="salve"}:Latin))

But we are still not done! F# is super confused. It's defined myCard as a tuple of English and Latin, but it's not saying it's a Card. Guess I'd better help:

let myCard = (({text="hello"}:English) , ({text="salve"}:Latin)):Card

OK. That worked, but it's super ugly. The reason it is ugly is:

1) The two sides of the card are identical, so there is no real need to have a Latin or English class distinction at this stage of the program, and we're trying to kinda force one in there

2) The card class itself is not doing storing anything that interesting that couldn't be represented as a Tuple anyway.

3) Probably most importantly; Inheritance is a pattern to avoid in functional programming.

Now, there is a slightly better way to do this - if we just say the whole thing is a Card, it is much happier:

let myCard = ({text="hello"} , {text="salve"}):Card

However, I think we can do better.

The second mistake: still defining types wrong

We're going to make a small adjustment to the structure. Instead of defining the generalised concept of a "slide", we're going to do the reverse and start with the specific types:

type MaleFemaleOrNeuter = Male | Female | Neuter
type EnglishSide = {text:string}
type LatinSide = {text:string; gender:MaleFemaleOrNeuter option}

type Card = EnglishSide * LatinSide

Note, we are creating more specific types "EnglishSide" and "LatinSide" that have slightly different data structures. The Latin language has a concept of nouns and adjectives having some kind of "gender". F# provides the "option" keyword to make nullable-like type parameters - however these parameters are not defaulted to be Nothing/null (or None in F#), and when we populate the property, we have to specifically say we have "Some" of that optional keyword.

let myCard = Card({text="hello"}, {text="salve"; gender = Some Male})

Note, the Card constructor is a convenient default that looks a little nicer. You can name the constructor something else if you wish:

type Card = C of EnglishSide * LatinSide	//define type with constructor C

let myCard = C({text="hello"}, {text="salve"; gender = Some Male})	//call constructor.

Expanding Capabilities

Let's add more languages!

As we are now expanding the concept of a Card to be a little more generic, I think we can re-introduce the concept of a Side, alongside other properties unique to the language:

module CardTypes

type MaleOrFemale = Male | Female
type MaleFemaleOrNeuter = Male | Female | Neuter

type EnglishSide = {text:string}

type LatinSide = {
    text:string ;
    gender:MaleFemaleOrNeuter option;   //Latin nouns may come in three genders
}

type ItalianSide = {
    text:string;
    gender: MaleOrFemale option;    //Italian nouns may be male or female (no neuter)
}

type Side =
    | E of EnglishSide
    | L of LatinSide
    | I of ItalianSide

type Card = Side * Side


let myCard =
    Card(
        E({text="hello"}),
        L({text="salve"; gender=None}))
let myCard2 =
    Card(
        L({text="mensa"; gender=Some Female}),
        I({text="tavolo"; gender=Some MaleOrFemale.Male}))

One cool thing I found out is that if I define the "E" constructor for the EnglishSide at the declaration of EnglishSide type, then E will return an EnglishSide type, but if I define the E constructor on the Side type declaration (as per above), then E returns a Side, rather than an EnglishSide. This makes the Card declaration a bit neater.

On the less favourable side, despite being somewhat obvious to me, F# was unable to distinguish that I was not passing a MaleFemaleOrNeuter option through to the gender for Italian on it's own, so I had to explicitly state this was the case.

The solution was to define constructors for the specific types against the Side type, so that it knows I'm intending it to be a Side:

type Side =
    | E of EnglishSide
    | L of LatinSide
    | I of ItalianSide

type Card = Side * Side


let myCard =
	Card(
    	E({english="hello"}),
        L({latin="salve"; gender=None}))
let myCard2 =
    Card(
        I({italian="tavolo"; gender=Some MaleOrFemale.Male}),
        L({latin="mensa"; gender=Some MaleFemaleOrNeuter.Female}))

Some more oddness here - option types can be defined as None or Some of something. So when I want to say I have a value, I have to explicitly say I have "Some" value.

Although this works, I went wondering down a long road to figure out if there was a way to specify a type that is Gender with extra constraints. There is, and it isn't pretty:

type Gender = Male | Female | Neuter

module BinaryGender = 
    type T = BinaryGender of Gender
    let create (g:Gender) =
        if g <> Neuter
        then Some (BinaryGender g)
        else None

...


type ItalianSide = {
    text:string;
    gender: BinaryGender.T option;    //Italian nouns may be male or female (no neuter)
}

I'm creating a grouping of concepts or a "module" called Binary Gender, and creating a type "T" underneath it, which is public. Then I'm defining a function called "create" that checks if the gender is a valid value. If it's not value it returns None otherwise it returns Some BinaryGender. This kinda works, though the syntax of use is a bit wordy:

let myCard2 =
    Card(
        L({text="mensa"; gender=Some Female}),
        I({text="tavolo"; gender=BinaryGender.create Male}))

Lesson learned - Records are not Classes

So much of this weirdness around trying to get a Card to have sides of different related / inherited classes is because I'm using the record type. I went away and researched some more and discovered that Records are compiled to sealed types. If I really want to use inheritance structures in F#, I'm better of using the equivalent of a class. This is done as follows:

type Side (text:string) =
    member this.text:string = text
    
type EnglishSide(text:string) =
    inherit Side(text)
    
type LatinSide(text:string, gender:Gender option) =
    inherit Side(text)
    member this.gender:Gender option = gender
    
type ItalianSide(text:string, gender:BinaryGender.T option) =
    inherit Side(text)
    member this.gender:BinaryGender.T option = gender

type Card = Side * Side

type Deck = array<Card>
let sourceDeck:Deck =
    [|
        Card(EnglishSide("hello"),
            LatinSide("salve", None));
        Card(LatinSide("mensa", Some Female),
            ItalianSide("tavola", BinaryGender.create Female));
        Card(LatinSide("nomen", Some Neuter),
            ItalianSide("nome", BinaryGender.create Male));
    |]

Now to actually do something

So far, I've just been mucking around with representing the data. Let's start to actually do something by returning to the main function. What I want is basically to show the text of the card, and then allow the user to flip the card to see the other side, and whatever information is presented for the card.

I started off with a function that reads a side of a card, and at least this was fairly simple:

let Read (side:Side) =
    sprintf "The word is %s" side.text

However, I realised that I need a way to tell the user what language the card is in, so I made a function for that and amended Read appropriately:

let getLanguageFor (s:Side) =
    match s with
    | (:? LatinSide as l) -> Some "Latin"
    | (:? EnglishSide as l) -> Some "English"
    | (:? ItalianSide as l) -> Some "Italian"
    | _ -> None

let Read (side:Side) =
    let language = getLanguageFor side
    match language with
        | Some lang -> sprintf "The %s word is %s" lang side.text
        | None -> sprintf "The (unknown) word is %s" side.text

I admit I had to look up how match expressions worked to figure this one out. You'll also note the getLanguageFor function returns "Some" or "None" of something, and we check if Some or None is returned to figure out how to read the card. I could expand this later to learn how to read more  information from the card maybe.

Then I created some functions that read different sides from a card. I think maybe there was a simpler way to do this with some syntax sugar, but this works well:

let SideAOf (c:Card) =
    let sideA, _ = c
    sideA
         
let SideBOf (c:Card) =
    let _, sideB = c
    sideB

Then, I added in abilities for handling the deck of cards - namely drawing and discarding so that we know when we're out of cards and don't keep re-drawing the same card over and over. It's also more analogous to real life than a for array (which I also don't know how to do yet in F#).

let anyIn (a:Deck) = a.Length > 1

let discardCardFrom (deck:Deck) =
    if anyIn deck
    then deck.[1..]:Deck   //return rest of array.
    else [||]:Deck  //return empty array

let drawCardFrom (deck:Deck) =
    if anyIn deck
    then Some deck.[0]
    else None
    

Now, to revisit the main function. We draw from the deck and read the card and keep going until we're out of cards:

[<EntryPoint>]
let main argv =
    printfn "Welcome to F#LASHCARDS"
    let mutable deck = CardTypes.sourceDeck
    let mutable card = drawCardFrom deck
    let writeLn s = Console.WriteLine(s:string)
    while card <> None do
        (writeLn << Read << SideAOf) card.Value
        let _ = Console.ReadKey()
        (writeLn << Read << SideBOf) card.Value
        writeLn ""
        deck <- discardCardFrom deck
        card <- drawCardFrom deck
    writeLn "No more cards."
    0

What I like most about this i that it's fairly readable without any kind of F# knowledge, almost like pseudeo-code, though the pipe parts with "<<"  are a bit cryptic maybe. I figured out this was the way F# liked to have a value from a function move into another from a you-tube video, but it's basically the same as this:

writeLn(Read(SideAOf card.Value))

Note the mutable keyword here allows deck to be reassigned to - by default everything is not mutable, meaning the line of deck -> discardCardFrom deck would never work, as that "mutates" the value. In most OO languages, it's the opposite behaviour - making a property or variable non-mutable requries a "readonly" keyword or similar.

That is it for me for the moment. I may experiment further, but for now - here's the completed Program:

// Learn more about F# at http://docs.microsoft.com/dotnet/fsharp

open System
open CardTypes

// Define a function to construct a message to print

let anyIn (a:Deck) = a.Length > 1

let discardCardFrom (deck:Deck) =
    if anyIn deck
    then deck.[1..]:Deck   //return rest of array.
    else [||]:Deck  //return empty array

let drawCardFrom (deck:Deck) =
    if anyIn deck
    then Some deck.[0]
    else None
    
let getLanguageFor (s:Side) =
    match s with
    | (:? LatinSide as l) -> Some "Latin"
    | (:? EnglishSide as l) -> Some "English"
    | (:? ItalianSide as l) -> Some "Italian"
    | _ -> None
    

let Read (side:Side) =
    let language = getLanguageFor side
    match language with
        | Some lang -> sprintf "The %s word is %s" lang side.text
        | None -> sprintf "The (unknown) word is %s" side.text
 
let SideAOf (c:Card) =
    let sideA, _ = c
    sideA
         
let SideBOf (c:Card) =
    let _, sideB = c
    sideB

[<EntryPoint>]
let main argv =
    printfn "Welcome to F#LASHCARDS"
    let mutable deck = CardTypes.sourceDeck
    let mutable card = drawCardFrom deck
    let writeLn s = Console.WriteLine(s:string)
    while card <> None do
        (writeLn << Read << SideAOf) card.Value
        let _ = Console.ReadKey()
        (writeLn << Read << SideBOf) card.Value
        writeLn ""
        deck <- discardCardFrom deck
        card <- drawCardFrom deck
    writeLn "No more cards."
    0