This commit is contained in:
Orion Kindel
2024-09-29 11:28:06 -05:00
parent 3ee2a9bbbd
commit 6bb54eb9d9
14 changed files with 296 additions and 36 deletions

View File

@@ -13,8 +13,8 @@
"state": {
"type": "markdown",
"state": {
"file": "Language/Functions/Defining/Guard Clause.md",
"mode": "source",
"file": "Language/Data Structures/type.md",
"mode": "preview",
"source": true
}
}
@@ -85,7 +85,7 @@
"state": {
"type": "backlink",
"state": {
"file": "Language/Functions/Defining/Guard Clause.md",
"file": "Language/Data Structures/type.md",
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
@@ -102,7 +102,7 @@
"state": {
"type": "outgoing-link",
"state": {
"file": "Language/Functions/Defining/Guard Clause.md",
"file": "Language/Data Structures/type.md",
"linksCollapsed": false,
"unlinkedCollapsed": true
}
@@ -125,7 +125,7 @@
"state": {
"type": "outline",
"state": {
"file": "Language/Functions/Defining/Guard Clause.md"
"file": "Language/Data Structures/type.md"
}
}
}
@@ -150,42 +150,42 @@
},
"active": "6465d16034124b8f",
"lastOpenFiles": [
"Language/Functions/Defining/Pattern Matching.md",
"Language/Functions/Applying.md",
"Language/Functions/Composition.md",
"Language/Functions/Currying.md",
"Language/Functions.md",
"Terminology/Point-free.md",
"Language/Infix Operators/Common Operators/Applying & Composing Functions.md",
"Language/Functions/Defining.md",
"Language/Expressions/case .. of.md",
"Language/Expressions/Lambda Functions.md",
"Language/Functions/Defining/Guard Clause.md",
"Language/Expressions/let .. in ...md",
"Language/Expressions/if .. then .. else ...md",
"Language/Expressions/do notation.md",
"Classes/Functor/Apply.md",
"Classes/Functor/Applicative.md",
"Classes/Bind/Monad.md",
"Language/Data Structures/data.md",
"Language/Data Structures/Records.md",
"Language/Data Structures/newtype.md",
"Monads/Identity.md",
"Monads/Untitled",
"Data/Void.md",
"Data/Unit.md",
"Examples/Chess Engine/002 Moves.md",
"Examples/Chess Engine/001 Types.md",
"Examples/Chess Engine/000 The problem.md",
"Data/Collections/Array.md",
"Classes/Bind/Bind.md",
"Classes/Alternative/Alt.md",
"Classes/Alternative/Plus.md",
"Monads/Transformers/MaybeT.md",
"Monads/Transformers/ExceptT.md",
"Monads/Transformers/ReaderT.md",
"Monads/Transformers/WriterT.md",
"Monads/Transformers/StateT.md",
"Monads/Transformers",
"Classes/Alternative/Alternative.md",
"Classes/Alternative/Alt.md",
"Classes/Bind/Monad.md",
"Classes/Enum/Bounded.md",
"Classes/Functor/Applicative.md",
"Classes/Functor/Functor.md",
"Classes/Functor/Apply.md",
"Classes/Math/DivisionRing.md",
"Classes/Foldable.md",
"Classes/Hashable.md",
"Classes/Enum/Enum.md",
"Classes/Enum/BoundedEnum.md",
"Classes/Enum",
"Language/Generics",
"Language/Functions/Defining/Guard Clause.md",
"Examples/Chess Engine",
"Examples",
"Language/Functions/Defining/Pattern Matching.md",
"Monads/Transformers",
"Classes/Collapsing",
"Classes/Bind",
"Classes/Functor",
"Classes/Alternative",
"Data/Product",
"Data/Sum",
"Data/Collections",
"Language/Row Types",
"Classes/Math",
"Untitled 1.canvas",
"Untitled.canvas"
]

View File

View File

0
fp/Classes/Enum/Enum.md Normal file
View File

0
fp/Classes/Hashable.md Normal file
View File

View File

@@ -1,2 +0,0 @@
# What
# Why

0
fp/Data/Unit.md Normal file
View File

0
fp/Data/Void.md Normal file
View File

View File

@@ -0,0 +1 @@
In this example we'll write a framework for writing chess engines

View File

@@ -0,0 +1,95 @@
Type-driven development is a methodology of modeling domain-specific data as types, for example a chess engine framework will need moves, sides (white/black), pieces, the board and piece coordinates.
By thinking about how we'll model this for a bit and starting with this step, we'll be able to have powerful but simple abstractions "fall out" of the design later that will make our life very easy.
Here's how I might start out:
```haskell
data Side = White | Black
derive instance Generic Side _
derive instance Eq Side
data Piece
= Pawn
| Knight
| Bishop
| Rook
| Queen
| King
derive instance Generic Side _
derive instance Eq Side
data File = A | B | C | D | E | F | G
derive instance Generic File _
derive instance Eq File
derive instance Ord File
instance Enum File where -- <snip>
instance Bounded File where -- <snip>
instance BoundedEnum File where -- <snip>
data Rank = R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8
derive instance Generic Rank _
derive instance Eq Rank
derive instance Ord Rank
instance Enum Rank where -- <snip>
instance Bounded Rank where -- <snip>
instance BoundedEnum Rank where -- <snip>
data Pos = Pos File Rank
derive instance Generic Pos _
derive instance Eq Pos
derive instance Ord Pos
data MoveKind = To | Promotes Piece
derive instance Generic MoveKind _
derive instance Eq MoveKind
data Move = Move Pos MoveKind Pos
derive instance Generic Move _
derive instance Eq Move
data Board = Board (Map Pos (Side /\ Piece))
derive instance Generic Board _
derive instance Eq Board
data Game = Game (NonEmptyArray Board)
derive instance Generic Game _
```
The starting board would look something like:
```haskell
boardInit :: Board
boardInit =
let
pieces rank side =
[ Pos A rank /\ side /\ Rook
, Pos B rank /\ side /\ Knight
, Pos C rank /\ side /\ Bishop
, Pos D rank /\ side /\ Queen
, Pos E rank /\ side /\ King
, Pos F rank /\ side /\ Bishop
, Pos G rank /\ side /\ Knight
, Pos H rank /\ side /\ Rook
]
pawns rank side =
(upFromIncluding bottom :: Array File)
<#> \file -> Pos file rank /\ side /\ Pawn
in
Board
$ Map.fromFoldable
$ pieces R1 White
<> pawns R2 White
<> pawns R7 Black
<> pieces R8 Black
gameInit :: Game
gameInit = Game $ pure boardInit
```
> [!tip]- Performance Optimization
> [[Map]] is a B-tree map, meaning lookups use bifurcation in `O(log(n))` time. To do position lookups in constant time, we could replace Map with [[HashMap]], and add [[Hashable]] implementations to `Pos`.
> [!tip]- Enums for rank & file
> A temptation may be to use [[Int]] for encoding rank & file in positions, but this could quickly get hairy with legal bounds-checking.
> By encoding X & Y as enums `Rank` & `File`, we've made it impossible to put illegal positions into the `Board`.

View File

@@ -0,0 +1,69 @@
Now that we have the data modeled, we can start with the bottom-most abstractions and build from there.
Some questions we'll need to answer:
- what legal moves does a piece have?
- what is the material balance of the game?
- is there a check or mate in this position?
In order to move, though, we'll need to be able to add integers to ranks and files:
> [!tip]- toEnum
> [[BoundedEnum]] provides `toEnum`, which happens to be a really quick way of solving the "into & out of" integers for these 2 datatypes.
> One difference is that [[BoundedEnum]]'s int representation is 0-based, but I think it makes more sense to have these be 1-based, so we subtract 1 before calling `toEnum` and add 1 after `fromEnum`.
```haskell
rankFromInt :: Int -> Maybe Rank
rankFromInt = toEnum <<< (_ - 1)
fileFromInt :: Int -> Maybe File
fileFromInt = toEnum <<< (_ - 1)
rankToInt :: Rank -> Int
rankToInt = (_ + 1) <<< fromEnum
fileToInt :: File -> Int
fileToInt = (_ + 1) <<< fromEnum
addRank :: Rank -> Int -> Maybe Rank
addRank a by = rankFromInt $ rankToInt a + by
addFile :: File -> Int -> Maybe File
addFile a by = fileFromInt $ fileToInt a + by
```
Now we can write a simple low-level `possibleMoves` function that yields all moves for a piece that keeps it within the bounds of the board (we'll check later if that is illegal for other reasons)
```haskell
possibleMoves :: Side -> Piece -> Pos -> Array Pos
possibleMoves Black Pawn (Pos r f)
| r == R7 = [Pos R6 f, Pos R5 f]
| otherwise =
maybe [] pure
$ (flip Pos f) <$> addRank r (-1)
possibleMoves White Pawn (Pos r f)
| r == R2 = [Pos R3 f, Pos R4 f]
| otherwise =
maybe [] pure
$ (flip Pos f) <$> addRank r 1
possibleMoves _ Knight (Pos r f) =
Array.catMaybes
[ Just Pos <*> addRank (-1) r <*> addFile (-2) f
, Just Pos <*> addRank (-2) r <*> addFile (-1) f
, Just Pos <*> addRank (1) r <*> addFile (-2) f
, Just Pos <*> addRank (2) r <*> addFile (-1) f
, Just Pos <*> addRank (-1) r <*> addFile (2) f
, Just Pos <*> addRank (-2) r <*> addFile (1) f
, Just Pos <*> addRank (1) r <*> addFile (2) f
, Just Pos <*> addRank (2) r <*> addFile (1) f
]
possibleMoves _ King (Pos r f) =
Array.catMaybes
[ Just Pos <*> addRank (-1) r <*> addFile (-1) f
, Just Pos <*> addRank 1 r <*> addFile (-1) f
, Just Pos <*> addRank (-1) r <*> addFile 1 f
, Just Pos <*> addRank 1 r <*> addFile 1 f
, Just Pos <*> addRank 0 r <*> addFile 1 f
, Just Pos <*> addRank 0 r <*> addFile (-1) f
, Just Pos <*> addRank 1 r <*> addFile 0 f
, Just Pos <*> addRank (-1) r <*> addFile 0 f
]
```

View File

@@ -0,0 +1,97 @@
`data` is how data structures are defined.
Data structures have a type name and any number of constructors, special functions that store data.
## Syntax
```haskell
data <type_name> <type_parameters>
= <constructor>
| ...
```
### type_name
a PascalCase identifier that is the type of the data structure
### type_parameters
space-separated list of 0 or more generic type variables
### constructor
a PascalCase identifier with 0 or more arguments. This identifier is used as a function that constructs data of type `type_name`.
## Data Structure Types
### Newtype
A newtype is a name for a data structure with one constructor accepting one argument.
Newtypes are used to add or customize behavior for types which can't otherwise be extended.
```haskell
data Identity a = Identity a
```
[[Identity]] is a very common newtype, allowing the use of [[do notation]] without using an actual monad that needs to be evaluated like [[Effect]] or [[Maybe]].
This pattern is so common and useful that there is a special subset of `data` structures specifically for writing newtypes; the [[newtype]] keyword.
### Sum Type
A [sum type](https://en.wikipedia.org/wiki/Tagged_union) is a data structure that may be in one of multiple possible states.
When you have an instance of a sum type and you want to handle a particular case, you need to exhaustively handle all cases.
```haskell
data Maybe a = Just a | Nothing
```
[[Maybe]] and [[Either]] are common examples of sum types, and their "or-ness" is what gives them their meaning.
### Product Type
While sum types encode "A or B", product types encode "A and B."
```haskell
data Tuple a b = Tuple a b
```
[[Tuple|Tuples]], for example, are encoded this way. In order to construct a tuple, you need to provide both fields.
## Examples
#### No constructors
```haskell
data Void
```
[[Void]] has no constructors, so it can **never** be created.
#### One constructor
```haskell
data IntNonZero = INZ Int
```
> [!hint]- Constructor Naming
> In this example I've named the type `IntNonZero` and its constructor `INZ` differently to show that constructors do not have to have the same name as the type they construct.
>
> If this were an actual type, I would probably name the constructor `IntNonZero` instead of `INZ`, though.
Here, we create a type `IntNonZero` that can be created by calling `INZ :: Int -> IntNonZero` with an [[Int]].
#### Multiple constructors
```haskell
data AnimalType = Cow | Dog | Cat
```
`AnimalType` has 3 possible constructors: `Cow`, `Dog` and `Cat`.
#### Parameterized
```haskell
data Identity a = Identity a
```
Declare a type [[Identity]], generic over some other type `a`.
You can create one with the constructor `Identity`, e.g.
```haskell
Identity 1
Identity 1.0
Identity ""
Identity []
Identity unit
```

View File

0
fp/Monads/Identity.md Normal file
View File