-
Notifications
You must be signed in to change notification settings - Fork 0
/
Program.cs
677 lines (538 loc) · 22.4 KB
/
Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
/**
# Abstract
Writing C# functional code has become easier with each new release of the language (i.e. nullable ref types, tuples, switch expr, ...).
This document presents a review of the current status of *basic* functional features for C# 8.0.
It focuses mostly on syntax and aims to achieve its goal using code examples.
It doesn't touches on more advanced topics as Monad, Functors, etc ...
Haskell has been chosen as the 'comparison' language (using few examples from [here](http://learnyouahaskell.com/) and elsewhere).
This is not intended as a statement of value of one language vs the other.
The languages are profoundly different in underlying philosophies.
They both do much more than what is presented here (i.e. C# supports OO and imperative styles, while Haskell goes much deeper in type system power etc...).
I also present samples of usage of [language-ext](https://github.com/louthy/language-ext) to show what can be achieved in C# using a functional library.
**/
/**
## Using a library of functions
In C# you can use static functions as if they were 'floating' by importing the static class they are defined to with the syntax `using static CLASSNAME`. Below an example of both importing `Console.WriteLine` and using it.
**/
using System;
using System.IO;
using LanguageExt;
using LanguageExt.TypeClasses;
using LanguageExt.ClassInstances;
using System.Collections.Generic;
using static System.Console;
using static System.Linq.Enumerable;
using static System.Math;
using static LanguageExt.Prelude;
public static class Core {
static void UseFunc() => WriteLine("System.WriteLine as a floating function");
/**
## Writing simple functions
In Haskell, a simple function might be written as:
```haskell
square :: Num a => a -> a
square x = x * x
```
Which in C# looks like the below.
**/
static int Square(int x) => x * x;
/**
As an aside, note that we lose the generality of the function (i.e. we need a different one for doubles).This is due to the lack of ad-hoc polymorphism in C#.
By using language-ext, you can fake it so that it looks like this:
**/
static A Square<NumA, A>(A x) where NumA : struct, Num<A> => default(NumA).Product(x, x);
static void SquarePoly() {
WriteLine(Square<TInt, int>(2));
WriteLine(Square<TDouble, double>(2.5));
}
/**
In Haskell, it is conventional to write the type of a function before the function itself. In C# you can use `Func` and `Action` to
achieve a similar goal (aka seeing the types separately from the function implementation), despite with more verbose syntax, as below:
**/
static Func<int, int> SquareF = x => x * x;
/**
## Pattern matching
Here is an example in Haskell:
```haskell
lucky :: (Integral a) => a -> String
lucky 7 = "LUCKY NUMBER SEVEN!"
lucky x = show x
```
In C#, you can write it in a few ways. Either as a function static property ...
**/
static Func<int, string> Lucky = x => x switch {
7 => "LUCKY NUMBER SEVEN!",
_ => x.ToString()
};
/**
Or with a normal function:
**/
static string Lucky1(int x) => x switch {
7 => "LUCKY NUMBER SEVEN!",
_ => x.ToString()
};
/**
Or even using the ternary operator (a perennial favorite of mine).
**/
static string Lucky2(int x) => x == 7 ? "LUCKY NUMBER SEVEN!"
: x.ToString();
/**
Pattern matching on tuples works with similar syntax:
**/
static bool And(bool x, bool y) =>
(x, y) switch
{
(true, true) => true,
_ => false
};
/**
In Haskell you can match on lists as follows:
```haskell
sum :: Num a => [a] -> a
sum [] = 0
sum (x:xs) = x + sum xs
```
Writing this in standard C# looks like this:
**/
static int Sum(IEnumerable<int> l) => l.Count() == 0
? 0
: l.First() + Sum(l.Skip(1));
/**
Or this:
**/
static int Sum1(IEnumerable<int> l) => l.Count() switch {
0 => 0,
_ => l.First() + Sum(l.Skip(1))
};
/**
Language-ext gives you a simpler syntax, with more flexibility on what you can match against:
**/
static int Sum2(Seq<int> l) =>
match(l,
() => 0,
(x, xs) => x + Sum1(xs));
/**
Obviously you always have to be careful with recursion in C# ([here](https://github.com/dotnet/csharplang/issues/2544)).
It is better to use the various methods on `Enumerable` (i.e. `Aggregate`) that abstract common recursion patterns.
## Guards (and case expressions)
Let's explore guards. Haskell `Case` expressions have an identical translation in C#.
In Haskell guards are used as below:
```haskell
bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
| weight / height ^ 2 <= 18.5 = "Under"
| weight / height ^ 2 <= 25.0 = "Normal"
| weight / height ^ 2 <= 30.0 = "Over"
| otherwise = "Way over"
```
Which can be coded in C# as:
**/
static string BmiTell(double weight, double height) =>
(weight, height) switch
{
_ when Pow(weight / height, 2) <= 18.5 => "Under",
_ when Pow(weight / height, 2) <= 25.0 => "Normal",
_ when Pow(weight / height, 2) <= 30.0 => "Over",
_ => "Way over"
};
/**
Obviously this is quite bad. You would like something more like:
```haskell
bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
| bmi <= skinny = "Under"
| bmi <= normal = "Normal"
| bmi <= fat = "Over"
| otherwise = "Way Over"
where bmi = weight / height ^ 2
skinny = 18.5
normal = 25.0
fat = 30.0
```
But it is not trivial in C# to declare variables in expression bodied members.
You can either move it to a normal method or abuse the LINQ query syntax. Both shown below.
Notice that this is more similar to `let` expressions in Haskell, as they come before the expression, not after.
**/
static string BmiTell1(double weight, double height) {
double bmi = Pow(weight / height, 2),
skinny = 18.5,
normal = 25.0,
fat = 30;
return bmi switch
{
_ when bmi <= skinny => "Under",
_ when bmi <= normal => "Normal",
_ when bmi <= fat => "Over",
_ => "Way over"
};
}
/**
In LINQ query syntax you get to keep your expression bodied member ...
**/
static string BmiTell2(double weight, double height) =>
( from _ in "x"
let bmi = Pow(weight / height, 2)
let skinny = 18.5
let normal = 25.0
let fat = 30
select bmi <= skinny ? "Under"
: bmi <= normal ? "Normal"
: bmi <= fat ? "Over"
: "Way over").First();
/**
## Product types (aka Records)
In Haskell you define a product type as below:
```haskell
data Person = Person { firstName :: String
, lastName :: String
, age :: Int
} deriving (Show)
```
In C#, it is currently complicated to define an immutable product type with structural equality, structural ordering and efficient hashing.
If you are certain that you are not going to compare your records and trust 'immutability by convention', then the syntax is not bad:
**/
struct Person {
public string FirstName;
public string LastName;
public int Age;
}
/**
And to create one, it is just:
**/
static Person CreatePersonExample() => new Person {
FirstName = "Rob",
LastName = "Smith",
Age = 40
};
/**
But if you want to do it right, you have to implement a bunch of interfaces and operators, somehow similar to below (and I am not implementing ordering, and it is probably not too efficient either).
**/
public readonly struct PersonData: IEquatable<PersonData> {
public readonly string FirstName;
public readonly string LastName;
public readonly int Age;
public PersonData(string first, string last, int age) => (LastName, FirstName, Age) = (last, first, age);
public override int GetHashCode() => (FirstName, LastName, Age).GetHashCode();
public override bool Equals(object? other) => other is PersonData l && Equals(l);
public bool Equals(PersonData oth) => LastName == oth.LastName && FirstName == oth.FirstName && Age == oth.Age;
public static bool operator ==(PersonData lhs, PersonData rhs) => lhs.Equals(rhs);
public static bool operator !=(PersonData lhs, PersonData rhs) => !(lhs == rhs);
}
/**
If you use a `struct`, you are still open to someone simply newing it using the default constructor, that you can't make `private`.
Using a class (as below) avoids that, but loses the pass by value semantic.
**/
public class PersonData1 : IEquatable<PersonData1> {
public readonly string FirstName;
public readonly string LastName;
public readonly int Age;
public PersonData1(string first, string last, int age) => (LastName, FirstName, Age) = (last, first, age);
public override int GetHashCode() => (FirstName, LastName, Age).GetHashCode();
public override bool Equals(object? oth) => oth is PersonData l && Equals(l);
public bool Equals(PersonData1 other) => LastName == other.LastName && FirstName == other.FirstName && Age == other.Age;
public static bool operator ==(PersonData1 lhs, PersonData1 rhs) => lhs.Equals(rhs);
public static bool operator !=(PersonData1 lhs, PersonData1 rhs) => !(lhs == rhs);
}
/**
So, there is no easy fix. Using Language-ext you can do it more simply, by inheritance.
But it uses IL generation that is slow the first time around, which might be a problem in certain scenarios: I.e. Azure Functions.
Try running the code and notice the delay when IL generating.
**/
public class PersonData2 : Record<PersonData2> {
public readonly string FirstName;
public readonly string LastName;
public readonly int Age;
public PersonData2(string first, string last, int age) => (LastName, FirstName, Age) = (last, first, age);
}
/**
## Sum types (aka Discriminated Union)
In Haskell you write:
```haskell
data Shape =
Circle Float Float Float
| Rectangle Float Float Float Float
| NoShape
deriving (Show)
```
There is no obvious equivalent in C#, and different libraries has sprung up to propose possible solutions (but not language-ext)
(i.e. [here](https://github.com/Galad/CSharpDiscriminatedUnion) or [here](https://github.com/mcintyre321/OneOf)).
One possible 'pure language' implementation, not implementing structural equality/ordering/hash, follows:
**/
abstract class Shape {
public sealed class NoShape : Shape { }
public sealed class Circle : Shape {
internal Circle(double r) => Radius = r;
public readonly double Radius;
}
public sealed class Rectangle : Shape {
internal Rectangle(double height, double width) => (Height, Width) = (height, width);
public readonly double Height;
public readonly double Width;
}
}
static Shape.Circle Circle(double x) => new Shape.Circle(x);
static Shape.Rectangle Rectangle(double x, double y) => new Shape.Rectangle(x,y);
static Shape.NoShape NoShape() => new Shape.NoShape();
/**
You can then pattern match over it in various obvious ways:
**/
static double Area(Shape s) => s switch
{
Shape.NoShape _ => 0,
Shape.Circle {Radius: var r } => Pow(r, 2) * PI,
Shape.Rectangle r => r.Height * r.Width,
_ => throw new Exception("No known shape")
};
static void CalcAreas() {
var c = Circle(10);
var r = Rectangle(10, 3);
var n = NoShape();
WriteLine(Area(c));
WriteLine(Area(r));
WriteLine(Area(n));
}
/**
## Maybe (Or Option) type
In Haskell you write:
```Haskell
f::Int -> Maybe Int
f 0 = Nothing
f x = Just x
g::Maybe Int -> Int
g Nothing = 0
g (Just x) = x
```
In C#, this easily translates to `Nullable` value and reference types. Assume you have `Nullable = enabled` in your project
**/
static int? F(int i) => i switch {
0 => new Nullable<int>(),
_ => i
};
static int G(int? i) => i ?? 0;
/**
And for reference types ...
**/
static int? GetAge(PersonData1? p) => p?.Age;
static int GetAge2(PersonData1 p) => p?.Age ?? 0;
/**
## Main Method
Let's wrap all the samples with a `Main` function and then write a full program.
**/
static void Main() {
UseFunc();
SquarePoly();
WriteLine(Square(2) == SquareF(2));
WriteLine(Sum(new[] { 1, 2, 3, 4 }) == Sum1(new[] { 1, 2, 3, 4 }));
WriteLine(Sum1(new[] { 1, 2, 3, 4 }) == Sum2(new[] { 1, 2, 3, 4 }.ToSeq()));
WriteLine(BmiTell(80, 100) == BmiTell1(80, 100));
WriteLine(BmiTell(80, 100) == BmiTell2(80, 100));
WriteLine(new PersonData("Bob", "Blake", 40) == new PersonData("Bob", "Blake", 40));
WriteLine(new PersonData1("Bob", "Blake", 40) == new PersonData1("Bob", "Blake", 40));
WriteLine("Before IL gen");
WriteLine(new PersonData2("Bob", "Blake", 40) == new PersonData2("Bob", "Blake", 40));
WriteLine(new PersonData2("Alphie", "Blake", 40) <= new PersonData2("Bob", "Blake", 40));
WriteLine("Already genned");
WriteLine(new PersonData2("Bob", "Blake", 40) == new PersonData2("Bob", "Blake", 40));
WriteLine(new PersonData2("Alphie", "Blake", 40) <= new PersonData2("Bob", "Blake", 40));
CalcAreas();
Hangman.Core.MainHangman();
}
}
/**
## Full program
Let's finish with a semi-working version of Hangman,
from an exercise in [Haskell programming from first principles](http://haskellbook.com/),
just to get an overall impression of how the two languages look for slightly bigger things.
The exercise in the book is a fill-in-the-blanks kind of thing with the function names and types given,
so I don't think I butchered it too badly, but maybe not.
The Haskell code is:
```haskell
module Main where
import Control.Monad (forever) -- [1]
import Data.Char (toLower) -- [2]
import Data.Maybe (isJust) -- [3]
import Data.List (intersperse) -- [4]
import System.Exit (exitSuccess) -- [5]
import System.Random (randomRIO) -- [6]
type WordList = [String]
allWords :: IO WordList
allWords = do
dict <- readFile "data/dict.txt"
return (lines dict)
minWordLength :: Int
minWordLength = 5
maxWordLength :: Int
maxWordLength = 9
gameWords :: IO WordList
gameWords = do
aw <- allWords
return (filter gameLength aw)
where gameLength w =
let l = length (w :: String)
in l > minWordLength && l < maxWordLength
randomWord :: WordList -> IO String
randomWord wl = do
randomIndex <- randomRIO ( 0, length wl - 1)
return $ wl !! randomIndex
randomWord' :: IO String
randomWord' = gameWords >>= randomWord
data Puzzle = Puzzle String [Maybe Char] [Char]
instance Show Puzzle where
show (Puzzle _ discovered guessed) =
(intersperse ' ' $ fmap renderPuzzleChar discovered)
++ " Guessed so far: " ++ guessed
freshPuzzle :: String -> Puzzle
freshPuzzle s = Puzzle s (map (const Nothing) s) []
charInWord :: Puzzle -> Char -> Bool
charInWord (Puzzle s _ _) c = c `elem` s
alreadyGuessed :: Puzzle -> Char -> Bool
alreadyGuessed (Puzzle _ _ s) c = c `elem` s
renderPuzzleChar :: Maybe Char -> Char
renderPuzzleChar Nothing = '_'
renderPuzzleChar (Just c) = c
fillInCharacter :: Puzzle -> Char -> Puzzle
fillInCharacter (Puzzle word filledInSoFar s) c =
Puzzle word newFilledInSoFar (c : s)
where zipper guessed wordChar guessChar =
if wordChar == guessed
then Just wordChar
else guessChar
newFilledInSoFar =
zipWith (zipper c) word filledInSoFar
handleGuess :: Puzzle -> Char -> IO Puzzle
handleGuess puzzle guess = do
putStrLn $ "Your guess was: " ++ [guess]
case (charInWord puzzle guess, alreadyGuessed puzzle guess) of
(_, True) -> do
putStrLn "You already guessed that character, pick something else!"
return puzzle
(True, _) -> do
putStrLn "This character was in the word,filling in the word accordingly"
return (fillInCharacter puzzle guess)
(False, _) -> do
putStrLn "This character wasn't in the word, try again."
return (fillInCharacter puzzle guess)
gameOver :: Puzzle -> IO ()
gameOver (Puzzle wordToGuess _ guessed) =
if (length guessed) > 7 then do
putStrLn "You lose!"
putStrLn $ "The word was: " ++ wordToGuess
exitSuccess
else return ()
gameWin :: Puzzle -> IO ()
gameWin (Puzzle _ filledInSoFar _) =
if all isJust filledInSoFar then do
putStrLn "You win!"
exitSuccess
else return ()
runGame :: Puzzle -> IO ()
runGame puzzle = forever $ do
gameOver puzzle
gameWin puzzle
putStrLn $ "Current puzzle is: " ++ show puzzle
putStr "Guess a letter: "
guess <- getLine
case guess of
[c] -> handleGuess puzzle c >>= runGame
_ -> putStrLn "Your guess must be a single character"
main :: IO ()
main = do
word <- randomWord'
let puzzle = freshPuzzle (fmap toLower word)
runGame puzzle
```
Which loosely translate to the code below (pure C#, no language-ext) . A few comments:
1. I tried to keep the translation as 1:1 as possible.
2. I used expression bodied members for everything except IO returning function. That's a pleasing convention to me.
3. Things translate rather straightforwardly except for:
1. Cheated using a simple `struct` instead of a `Record`, but often it is ok to do so.
2. Needed to use LINQ query syntax to translate more complex expressions, but cannot have lambadas in it.
3. Needed to do manual currying (`language-ext` would have beautified that).
4. The line count for this example is roughly similar. I think that's random. Also the C# code 'extends to the right' more.
5. Notably absent from the code are sum types, which would have been verbose to implement in C#.
**/
namespace Hangman {
using WordList = IEnumerable<String>;
using static System.Linq.Enumerable;
static class Core {
const int MinWordLength = 5;
const int MaxWordLength = 9;
static WordList AllWords => File.ReadAllLines("data/dict.txt");
static WordList GameWords =>
AllWords.Where(w => w.Length > MinWordLength && w.Length < MaxWordLength);
static Random r = new Random();
static string RandomWord(WordList wl) => GameWords.ElementAt(r.Next(0, wl.Length()));
static string RandomWord1 => RandomWord(GameWords);
static char RenderPuzzleChar(char? c) => c ?? '_';
struct Puzzle { // Not implemented Eq and Ord because not needed in this program
public string Word;
public IEnumerable<char?> Discovered;
public string Guessed;
public override string ToString() =>
$"{string.Join(" ", Discovered.Select(RenderPuzzleChar))}"
+ " Guessed so far: " + Guessed;
}
static Puzzle FreshPuzzle(string s) => new Puzzle {
Word = s,
Discovered = s.Select(_ => new Nullable<char>()),
Guessed = ""
};
static bool CharInWord(Puzzle p, char c) => p.Word.Contains(c);
static bool AlreadyGuessed(Puzzle p, char c) => p.Guessed.Contains(c);
// Can't assign lambda expression to range variable with let, hence separate function
static char? Zipper(char guessed, char wordChar, char? guessChar) =>
wordChar == guessed ? wordChar : guessChar;
// Manual curry. Could use language-ext to make it more beautiful.
static Func<char, char?, char?> Zipper1(char c) => (c1, c2) => Zipper(c, c1, c2);
static Puzzle FillInCharacter(Puzzle p, char c) =>
(from _ in "x"
let newFilledInSoFar = System.Linq.Enumerable.Zip<char, char?, char?>(p.Word, p.Discovered, Zipper1(c))
select new Puzzle {
Word = p.Word,
Discovered = newFilledInSoFar,
Guessed = c + p.Guessed
}).First();
static Puzzle HandleGuess(Puzzle puzzle, char guess) {// Braces means IO ...
WriteLine($"Your guess was {guess}");
switch (CharInWord(puzzle, guess), AlreadyGuessed(puzzle, guess)) {
case (_, true):
WriteLine("You already guessed that character, pick something else!");
return puzzle;
case (true, _):
WriteLine("This character was in the word,filling in the word accordingly");
return FillInCharacter(puzzle, guess);
case (false, _):
WriteLine("This character wasn't in the word, try again.");
return FillInCharacter(puzzle, guess);
}
}
static void GameOver(Puzzle p) {
if(p.Guessed.Length > 7) {
WriteLine("You lose!");
WriteLine($"The word was {p.Word}");
Environment.Exit(0);
}
}
static void GameWin(Puzzle p) {
if(p.Discovered.All(c => c.HasValue)) {
WriteLine("You win!");
Environment.Exit(0);
}
}
static void RunGame(Puzzle puzzle) {
while(true) {
GameOver(puzzle);
GameWin(puzzle);
WriteLine($"Current puzzle is: {puzzle}");
WriteLine("Guess a letter: ");
var guess = ReadLine();
if(guess.Length == 1) RunGame(HandleGuess(puzzle, guess[0]));
else WriteLine("Your guess must be a single char");
}
}
public static void MainHangman() {
var puzzle = FreshPuzzle(RandomWord1);
RunGame(puzzle);
}
}
}