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:
- let keyword
- a name
- one or more arguments separated by spaces
- =, 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