Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat:Code Organization #25

Merged
merged 14 commits into from
Nov 9, 2023
Merged
86 changes: 68 additions & 18 deletions docs/fsharp-cheatsheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ Composition operator `>>` is used to compose functions:
let squareNegateThenPrint' =
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved
square >> negate >> print

### Recursive functions
<a name="Functions_RecursiveFunctions"></a>Functions
### Recursive Functions
The `rec` keyword is used together with the `let` keyword to define a recursive function:

let rec fact x =
Expand Down Expand Up @@ -551,6 +552,7 @@ Another way of implementing interfaces is to use *object expressions*.
<a name="CodeOrganization"></a>Code Organization
---------

<a name="CodeOrganization_Modules"></a>
### Modules
Modules are key building blocks for grouping related code; they can contain `types`, `let` bindings, or (nested) sub `module`s.
Identifiers within modules can be referenced using dot notation, or you can bring them into scope via the `open` keyword. Illustrative-only example:
Expand All @@ -567,25 +569,28 @@ Identifiers within modules can be referenced using dot notation, or you can brin
basePoints <- 2
let player'' = playerScored player' // player''.score = 3

If you have only one module in a file, the `module` name can be declared at the top of the file, with all further declarations being module elements (and non indentation required)
If you have only one module in a file, the `module` name can be declared at the top of the file, with all further declarations
being module elements (and non indentation required)
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved

module Functions // notice there is no '=' when at the top of a file

let addToFive num = 5 + num
let subtractFive num = num - 5

### Namespaces
Namespaces are simply dotted strings that prefix names. They are placed at the top of the file.
An inner namespace can be specified in the same file.
Namespaces are simply dotted names that prefix other program elements to allow for further hierarchical organization.
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved
All `type` and `module` elements that follow a `namespace` declaration will require an [`open`](#CodeOrganization_OpenAndAutoOpen) or to be dotted-into to access.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that follow -> within ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"following in the file" ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

demarcated by :D

I wanted to allude to a namespace being a grouping construct that things logically within the scope of

And pave the way for saying "and if you use namespace again, then it's an entirely separated new scope with nothing carried over"

I guess module and namespace have that aspect in common

If a new `namespace` is specified, all elements following will be part of the new namespace.
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved

namespace MyNamespace

namespace MyNamespace.SubNamespace

They can also be part of a module name.
They can also be specified in a file-level [`module`](#CodeOrganization_Modules) definition, but no further `namespace` declarations may follow.
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved

module MyNamespace.SubNamespace.Functions

<a name="CodeOrganization_OpenAndAutoOpen"></a>
### Open and AutoOpen

The `open` keyword can be used with `module`, `namespace`, and `type`.
Expand All @@ -602,7 +607,10 @@ The `open` keyword can be used with `module`, `namespace`, and `type`.
let duNotDefined = D // 'D' not defined
open MyModule
let du2 = D
---

Available to `module` elements, is the `AutoOpen` attribute. This alleviates the need for an `open`; *however* this should be used cautiously,
as all following declarations will be immediately brought into the global namespace and cause conflicts.
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved

[<AutoOpen>]
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved
module MyModule =
type DU = A | B | C
Expand All @@ -611,29 +619,71 @@ The `open` keyword can be used with `module`, `namespace`, and `type`.

### Accessibility Modifiers

F# supports `public`, `private` and `internal`. It can be applied to `module`, `let`, `member`, and `type`.
F# supports `public`, `private` (restraining the element to its containing `type` or `module`) and `internal` (restraining the element to its containing assembly).
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved
It can be applied to `module`, `let`, `member`, `type`, and `new`.
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved

module private MyModule = ...
---
let private x = ...
---
member private self.x = ...
---
type private Person = ...
With the exception of `let` bindings in a class `type`, everything defaults to `public`.

| Element | Example with Modifier |
|-------------------------------------------------------------------|--------------------------------------------|
| Module | `module internal MyModule =` |
| Module .. `let` | `let private value =` |
| Record | `type internal MyRecord = { id: int }` |
| Record [ctor](#CodeOrganization_PrivateConstructors) | `type MyRecord = private { id: int }` |
| Discriminated Union | `type private MyDiscUni = A \| B` |
| Discriminated Union [ctor](#CodeOrganization_PrivateConstructors) | `type MyDiscUni = internal A \| B ` |
| Class | `type internal MyClass() =` |
| Class [ctor](#CodeOrganization_PrivateConstructors) | `type MyClass private () =` |
| Class Additional [ctor](#CodeOrganization_PrivateConstructors) | `internal new() = MyClass("defaultValue")` |
| Class .. `let` | *Always private. Cannot be modified* |
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved
| `type` .. `member` | `member private _.classMember =` |

<a name="CodeOrganization_PrivateConstructors"></a>
##### Private Constructors

Limiting `type` constructors (ctor) accessibility is a good way to enforce value integrity.
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved

Example of Single-case Discriminated Union with a `private` constructor:

type UnitQuantity = private UnitQuantity of int
with
static member private maxQty = 100
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved
static member create (qty:int) : Result<UnitQuantity, string> =
if qty <= UnitQuantity.maxQty
then Ok (UnitQuantity qty)
else Error $"UnitQuantity cannot be more than {UnitQuantity.maxQty}"

let uQty = UnitQuantity.create 50
match uQty with
| Ok (UnitQuantity qty) -> printfn $"Good: {qty}"
| Error errStr -> printfn $"Bad: {errStr}"

Example of a class with a `private` constructor:

type MyClass private (count:int) =
member this.Count = count
static member CreateInstance (cnt: int) : MyClass option =
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved
if cnt > 0
then Some(new MyClass(cnt))
else None

let myClass = MyClass.CreateInstance (5)
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved

### Recursive Reference

F#'s type inference and name resolution runs in file and line order; by default, any forward references are considered as errors. This default provides a single benefit, which can be hard to appreciate initially: you never need to look beyond the current file for a dependency. In general this also nudges toward more careful design and organisation of codebases.
which results in cleaner, maintainable code, but in rare cases you may need to loosen those rules.
To do this we have `rec` for `module` and `namespace`s; and `and` for `type`s and `let` functions.
F#'s type inference and name resolution runs in file and line order; by default, any forward references are considered as errors.
This default provides a single benefit, which can be hard to appreciate initially: you never need to look beyond the current file for a dependency.
In general this also nudges toward more careful design and organisation of codebases,
which results in cleaner, maintainable code. However, in rare cases you may need to loosen those rules.
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved
To do this we have `rec` for `module` and `namespace`s; and `and` for `type` and [`let`](#Functions_RecursiveFunctions) functions.
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved

module rec MyNamespace.MonkeyDomain

exception DoNotSqueezeBananaException of Banana // `Banana` has not been defined yet, and would fail without `rec`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a valid example, but very esoteric

I'd say most common usage of rec is people saying "well for this file, I just want to list everything alphabetically, regardless of the fact that it's normally good to put low level stuff first and then have the hihger level stuff at the bottom, with some exceptions where you use and to put some irrelevant dependencies underneath"

So while this does illustrate what it does and how to use it, I dont think it conveys what it's for really, so am pretty conflicted.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I stole this from MS Learn. I kind of hate it too. The long names get in the way of what's being demonstrated as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a walk and realized I'm conflicted because I hate exceptions. :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


type Banana = { Type: string; IsRipe: bool }
with
member self.Squeeze() = raise (DoNotSqueezeBananaException self)
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved
and

See [Namespaces (MS Learn)](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/namespaces) and [Modules (MS Learn)](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/modules) to learn more.
SpiralOSS marked this conversation as resolved.
Show resolved Hide resolved

Expand Down