Take idiomatic C# and apply a few favorite patterns and concepts from functional languages like F# to make something hopefully more expressive, more elegant, and less bug-prone.
A talk by Bob Davidson for South Dakota Code Camp 2016.
4. What this talk is
A gentle introduction to functional
paradigms using a language you may
already be familiar with.
A comparison between OOP and
functional styles
A discussion on language expectations
5. What this talk
isn’t
“OOP is dead!”
“Functional all the things!”
“All code should look exactly like
this!” (Spoiler: it probably shouldn’t)
6. Who I am
Bob Davidson
C# / Web Developer 11 years
Blend Interactive
A guy who is generally interested in
and learning about functional
programming concepts
https://github.com/mrdrbob
7. Who I am Not
A functional programming expert who
says things like:
“All told, a monad in X is just a
monoid in the category of
endofunctors of X, with product ×
replaced by composition of
endofunctors and unit set by the
identity endofunctor.”
-Saunders Mac Lane
8. Let’s Build a Parser!
A highly simplified JSON-like syntax for strings and integers.
13. IntegerParser
public class IntegerParser : IParser<int> {
public bool TryParse(string raw, out int value) {
value = 0;
int x = 0;
List<char> buffer = new List<char>();
while (x < raw.Length && char.IsDigit(raw[x])) {
buffer.Add(raw[x]);
x += 1;
}
if (x == 0)
return false;
// Deal with it.
value = int.Parse(new string(buffer.ToArray()));
return true;
}
}
14. IntegerParser
public class IntegerParser : IParser<int> {
public bool TryParse(string raw, out int value) {
value = 0;
int x = 0;
List<char> buffer = new List<char>();
while (x < raw.Length && char.IsDigit(raw[x])) {
buffer.Add(raw[x]);
x += 1;
}
if (x == 0)
return false;
value = int.Parse(new string(buffer.ToArray()));
return true;
}
}
15. StringParser
public class StringParser : IParser<string> {
public bool TryParse(string raw, out string value) {
value = null;
int x = 0;
if (x == raw.Length || raw[x] != '"')
return false;
x += 1;
List<char> buffer = new List<char>();
while (x < raw.Length && raw[x] != '"') {
if (raw[x] == '') {
x += 1;
if (x == raw.Length)
return false;
if (raw[x] == '')
buffer.Add(raw[x]);
else if (raw[x] == '"')
buffer.Add(raw[x]);
else
return false;
} else {
buffer.Add(raw[x]);
}
x += 1;
}
if (x == raw.Length)
return false;
x += 1;
value = new string(buffer.ToArray());
return true;
}
}
16. Possible Issues
public class StringParser : IParser<string> {
public bool TryParse(string raw, out string value) {
value = null;
int x = 0;
if (x == raw.Length || raw[x] != '"')
return false;
x += 1;
List<char> buffer = new List<char>();
while (x < raw.Length && raw[x] != '"') {
if (raw[x] == '') {
x += 1;
if (x == raw.Length)
return false;
if (raw[x] == '')
buffer.Add(raw[x]);
else if (raw[x] == '"')
buffer.Add(raw[x]);
else
return false;
} else {
buffer.Add(raw[x]);
}
x += 1;
}
if (x == raw.Length)
return false;
x += 1;
value = new string(buffer.ToArray());
return true;
}
}
Repeated checks against
running out of input
Easily missed logic for
moving input forward
No way to see how much
input was consumed / how
much is left
Hard to understand at a
glance what is happening
public class IntegerParser : IParser<int> {
public bool TryParse(string raw, out int value) {
value = 0;
int x = 0;
List<char> buffer = new List<char>();
while (x < raw.Length && char.IsDigit(raw[x])) {
buffer.Add(raw[x]);
x += 1;
}
if (x == 0)
return false;
// Deal with it.
value = int.Parse(new string(buffer.ToArray()));
return true;
}
}
17. Rethinking the Parser
Make a little more generic / reusable
Break the process down into a series of rules which can be composed to make new
parsers from existing parsers
Build a framework that doesn’t rely on strings, but rather a stream of tokens
21. Ignore Latter
Keep Latter
Zero or More Times
Any of these
NotKeep Latter
A Parser Built on Rules (String Parser)
“
“
Keep Latter
Any of these
“
“
22. A Set of Rules
Match Quote
Match Slash
Match Digit
Match Then Keep
Match Then Ignore
Match Any
Match Zero or More Times
Match One or More Times
Not
23. Rethinking the Source
Handle tokens other than chars (such as byte streams, pre-lexed tokens, etc)
Need the ability to continue parsing after a success
Need the ability to reset after a failure
24. Rethinking the Source
public interface ISource<Token> {
Token Current { get; }
bool HasMore { get; }
int CurrentIndex { get; }
void Move(int index);
}
public class StringSource : ISource<char> {
readonly string value;
int index;
public StringSource(string value) { this.value = value; }
public char Current => value[index];
public int CurrentIndex => index;
public bool HasMore => index < value.Length;
public void Move(int index) => this.index = index;
}
25. Creating a Rule
public interface IRule<Token, TResult> {
bool TryParse(ISource<Token> source, out TResult result);
}
26. Char Matches...
public class CharIsQuote : IRule<char, char> {
public bool TryParse(ISource<char> source, out
char result) {
result = default(char);
if (!source.HasMore)
return false;
if (source.Current != '"')
return false;
result = source.Current;
source.Move(source.CurrentIndex + 1);
return true;
}
}
public class CharIs : IRule<char, char> {
readonly char toMatch;
public CharIs(char toMatch) { this.toMatch =
toMatch; }
public bool TryParse(ISource<char> source, out char
result) {
result = default(char);
if (!source.HasMore)
return false;
if (source.Current != toMatch)
return false;
result = source.Current;
source.Move(source.CurrentIndex + 1);
return true;
}
}
27. Char Matches...
public abstract class CharMatches : IRule<char, char> {
protected abstract bool IsCharMatch(char c);
public bool TryParse(ISource<char> source, out char result) {
result = default(char);
if (!source.HasMore)
return false;
if (!IsCharMatch(source.Current))
return false;
result = source.Current;
source.Move(source.CurrentIndex + 1);
return true;
}
}
public class CharIsDigit : CharMatches {
protected override bool IsCharMatch(char c) => char.IsDigit(c);
}
public class CharIs : CharMatches {
readonly char toMatch;
public CharIs(char toMatch) { this.toMatch = toMatch; }
protected override bool IsCharMatch(char c) => c == toMatch;
}
28. First Match (or Any)
public class FirstMatch<Token, TResult> : IRule<Token, TResult> {
readonly IRule<Token, TResult>[] rules;
public FirstMatch(IRule<Token, TResult>[] rules) { this.rules = rules; }
public bool TryParse(ISource<Token> source, out TResult result) {
foreach(var rule in rules) {
int originalIndex = source.CurrentIndex;
if (rule.TryParse(source, out result))
return true;
source.Move(originalIndex);
}
result = default(TResult);
return false;
}
}
29. Match Then... public abstract class MatchThen<Token, TLeft, TRight, TOut> : IRule<Token, TOut> {
readonly IRule<Token, TLeft> leftRule;
readonly IRule<Token, TRight> rightRule;
protected abstract TOut Combine(TLeft leftResult, TRight rightResult);
public MatchThen(IRule<Token, TLeft> leftRule, IRule<Token, TRight> rightRule) {
this.leftRule = leftRule;
this.rightRule = rightRule;
}
public bool TryParse(ISource<Token> source, out TOut result) {
int originalIndex = source.CurrentIndex;
result = default(TOut);
TLeft leftResult;
if (!leftRule.TryParse(source, out leftResult)) {
source.Move(originalIndex);
return false;
}
TRight rightResult;
if (!rightRule.TryParse(source, out rightResult)) {
source.Move(originalIndex);
return false;
}
result = Combine(leftResult, rightResult);
return true;
}
}
30. Match Then...
public class MatchThenKeep<Token, TLeft, TRight> : MatchThen<Token, TLeft, TRight, TRight> {
public MatchThenKeep(IRule<Token, TLeft> leftRule, IRule<Token, TRight> rightRule) : base(leftRule, rightRule) { }
protected override TRight Combine(TLeft leftResult, TRight rightResult) => rightResult;
}
public class MatchThenIgnore<Token, TLeft, TRight> : MatchThen<Token, TLeft, TRight, TLeft> {
public MatchThenIgnore(IRule<Token, TLeft> leftRule, IRule<Token, TRight> rightRule) : base(leftRule, rightRule) { }
protected override TLeft Combine(TLeft leftResult, TRight rightResult) => leftResult;
}
31. Invert Rule (Not)
public class Not<Token, TResult> : IRule<Token, Token> {
readonly IRule<Token, TResult> rule;
public Not(IRule<Token, TResult> rule) { this.rule = rule; }
public bool TryParse(ISource<Token> source, out Token result) {
result = default(Token);
if (!source.HasMore)
return false;
int originalIndex = source.CurrentIndex;
TResult throwAwayResult;
bool matches = rule.TryParse(source, out throwAwayResult);
if (matches)
{
source.Move(originalIndex);
return false;
}
source.Move(originalIndex);
result = source.Current;
source.Move(originalIndex + 1);
return true;
}
}
Spot the bug!
32. Many (Once, Zero, and more times)
public class Many<Token, TResult> : IRule<Token, TResult[]> {
readonly IRule<Token, TResult> rule;
readonly bool requireAtLeastOne;
public Many(IRule<Token, TResult> rule, bool requireAtLeastOne) { this.rule = rule; this.requireAtLeastOne = requireAtLeastOne; }
public bool TryParse(ISource<Token> source, out TResult[] results) {
List<TResult> buffer = new List<TResult>();
while (source.HasMore) {
int originalIndex = source.CurrentIndex;
TResult result;
bool matched = rule.TryParse(source, out result);
if (!matched) {
source.Move(originalIndex);
break;
}
buffer.Add(result);
}
if (requireAtLeastOne && buffer.Count == 0) {
results = null;
return false;
}
results = buffer.ToArray();
return true;
}
}
33. Map Result
public abstract class MapTo<Token, TIn, TOut> : IRule<Token, TOut> {
readonly IRule<Token, TIn> rule;
protected MapTo(IRule<Token, TIn> rule) { this.rule = rule; }
protected abstract TOut Convert(TIn value);
public bool TryParse(ISource<Token> source, out TOut result) {
result = default(TOut);
int originalIndex = source.CurrentIndex;
TIn resultIn;
if (!rule.TryParse(source, out resultIn)) {
source.Move(originalIndex);
return false;
}
result = Convert(resultIn);
return true;
}
}
34. Map Result
public class JoinText : MapTo<char, char[], string> {
public JoinText(IRule<char, char[]> rule) : base(rule) { }
protected override string Convert(char[] value) => new string(value);
}
public class MapToInteger : MapTo<char, string, int> {
public MapToInteger(IRule<char, string> rule) : base(rule) { }
protected override int Convert(string value) => int.Parse(value);
}
35. Putting the blocks together
var quote = new CharIs('"');
var slash = new CharIs('');
var escapedQuote = new MatchThenKeep<char, char, char>(slash, quote);
var escapedSlash = new MatchThenKeep<char, char, char>(slash, slash);
var notQuote = new Not<char, char>(quote);
var insideQuoteChar = new FirstMatch<char, char>(new[] {
(IRule<char, char>)escapedQuote,
escapedSlash,
notQuote
});
var insideQuote = new Many<char, char>(insideQuoteChar, false);
var insideQuoteAsString = new JoinText(insideQuote);
var openQuote = new MatchThenKeep<char, char, string>(quote,
insideQuoteAsString);
var fullQuote = new MatchThenIgnore<char, string, char>(openQuote, quote);
var source = new StringSource(raw);
string asQuote;
if (fullQuote.TryParse(source, out asQuote))
return asQuote;
source.Move(0);
int asInteger;
if (digitsAsInt.TryParse(source, out asInteger))
return asInteger;
return null;
var digit = new CharIsDigit();
var digits = new Many<char, char>(digit, true);
var digitsString = new JoinText(digits);
var digitsAsInt = new MapToInteger(digitsString);
43. An Immutable Source
public interface ISource<Token> {
Token Current { get; }
bool HasMore { get; }
int CurrentIndex { get; }
void Move(int index);
}
public interface ISource<Token> {
Token Current { get; }
bool HasMore { get; }
ISource<Token> Next();
}
44. An Immutable Source
public class StringSource : ISource<char> {
readonly string value;
int index;
public StringSource(string value) {
this.value = value; }
public char Current => value[index];
public int CurrentIndex => index;
public bool HasMore => index < value.Length;
public void Move(int index) => this.index = index;
}
public class StringSource : ISource<char> {
readonly string value;
readonly int index;
public StringSource(string value, int index = 0) {
this.value = value; this.index = index; }
public char Current => value[index];
public bool HasMore => index < value.Length;
public ISource<char> Next() =>
new StringSource(value, index + 1);
}
45. Ditch the Out
public class Result<Token, TValue> {
public bool Success { get; }
public TValue Value { get; }
public string Message { get; }
public ISource<Token> Next { get; }
public Result(bool success, TValue value, string message, ISource<Token> next) {
Success = success;
Value = value;
Message = message;
Next = next;
}
}
public interface IRule<Token, TValue> {
Result<Token, TValue> TryParse(ISource<Token> source);
}
46. Char Matches...
public abstract class CharMatches : IRule<char, char> {
protected abstract bool IsCharMatch(char c);
public bool TryParse(ISource<char> source, out char result) {
result = default(char);
if (!source.HasMore)
return false;
if (!IsCharMatch(source.Current))
return false;
result = source.Current;
source.Move(source.CurrentIndex + 1);
return true;
}
}
public abstract class CharMatches : IRule<char, char> {
protected abstract bool IsCharMatch(char c);
public Result<char, char> TryParse(ISource<char> source) {
if (!source.HasMore)
return new Result<char, char>(false, '0', "Unexpected EOF", null);
if (!IsCharMatch(source.Current))
return new Result<char, char>(false, '0', $"Unexpected char: {source.Current}", null);
return new Result<char, char>(true, source.Current, null, source.Next());
}
}
47. These Don’t Change
public class CharIsDigit : CharMatches {
protected override bool IsCharMatch(char c) => char.IsDigit(c);
}
public class CharIs : CharMatches {
readonly char toMatch;
public CharIs(char toMatch) { this.toMatch = toMatch; }
protected override bool IsCharMatch(char c) => c == toMatch;
}
48. First Match
public class FirstMatch<Token, TResult> : IRule<Token, TResult> {
readonly IRule<Token, TResult>[] rules;
public FirstMatch(IRule<Token, TResult>[] rules) { this.rules = rules; }
public bool TryParse(ISource<Token> source, out TResult result) {
foreach(var rule in rules) {
int originalIndex = source.CurrentIndex;
if (rule.TryParse(source, out result))
return true;
source.Move(originalIndex);
}
result = default(TResult);
return false;
}
}
public class FirstMatch<Token, TResult> : IRule<Token, TResult> {
readonly IRule<Token, TResult>[] rules;
public FirstMatch(IRule<Token, TResult>[] rules) { this.rules = rules; }
public Result<Token, TResult> TryParse(ISource<Token> source) {
foreach (var rule in rules) {
var result = rule.TryParse(source);
if (result.Success)
return result;
}
return new Result<Token, TResult>(false, default(TResult), "No rule matched", null);
}
}
49. Match Then...
public bool TryParse(ISource<Token> source, out TOut result) {
int originalIndex = source.CurrentIndex;
result = default(TOut);
TLeft leftResult;
if (!leftRule.TryParse(source, out leftResult)) {
source.Move(originalIndex);
return false;
}
TRight rightResult;
if (!rightRule.TryParse(source, out rightResult)) {
source.Move(originalIndex);
return false;
}
result = Combine(leftResult, rightResult);
return true;
}
public Result<Token, TOut> TryParse(ISource<Token> source) {
var leftResult = leftRule.TryParse(source);
if (!leftResult.Success)
return new Result<Token, TOut>(false, default(TOut), leftResult.Message, null);
var rightResult = rightRule.TryParse(leftResult.Next);
if (!rightResult.Success)
return new Result<Token, TOut>(false, default(TOut), rightResult.Message, null);
var result = Combine(leftResult.Value, rightResult.Value);
return new Result<Token, TOut>(true, result, null, rightResult.Next);
}
50. Invert Rule (Not)
public class Not<Token, TResult> : IRule<Token, Token> {
readonly IRule<Token, TResult> rule;
public Not(IRule<Token, TResult> rule) { this.rule = rule; }
public bool TryParse(ISource<Token> source, out Token result) {
result = default(Token);
if (!source.HasMore)
return false;
int originalIndex = source.CurrentIndex;
TResult throwAwayResult;
bool matches = rule.TryParse(source, out throwAwayResult);
if (matches)
{
source.Move(originalIndex);
return false;
}
source.Move(originalIndex);
result = source.Current;
source.Move(originalIndex + 1);
return true;
}
}
public class Not<Token, TResult> : IRule<Token, Token> {
readonly IRule<Token, TResult> rule;
public Not(IRule<Token, TResult> rule) { this.rule = rule; }
public Result<Token, Token> TryParse(ISource<Token> source) {
if (!source.HasMore)
return new Result<Token, Token>(false, default(Token), "Unexpected
EOF", null);
var result = rule.TryParse(source);
if (result.Success)
return new Result<Token, Token>(false, default(Token), "Unexpected
match", null);
return new Result<Token, Token>(true, source.Current, null,
source.Next());
}
}
52. Still Room for Improvement
public class Result<Token, TValue> {
public bool Success { get; }
public TValue Value { get; }
public string Message { get; }
public ISource<Token> Next { get; }
public Result(bool success, TValue value, string message, ISource<Token> next) {
Success = success;
Value = value;
Message = message;
Next = next;
}
}
Only valid when Success = true
Only valid when Success = false
54. Two States (Simple “Result” Example)
public interface IResult { }
public class SuccessResult<TValue> : IResult {
public TValue Value { get; }
public SuccessResult(TValue value) { Value = value; }
}
public class ErrorResult : IResult {
public string Message { get; }
public ErrorResult(string message) { Message = message; }
}
55. Two States (The Matching)
IResult result = ParseIt();
if (result is SuccessResult<string>) {
var success = (SuccessResult<string>)result;
Console.WriteLine($"SUCCESS: {success.Value}");
} else if (result is ErrorResult) {
var error = (ErrorResult)result;
Console.WriteLine($"ERR: {error.Message}");
}
56. Pattern Matching(ish)
public interface IResult<TValue> {
T Match<T>(Func<SuccessResult<TValue>, T> success,
Func<ErrorResult<TValue>, T> error);
}
public class SuccessResult<TValue> : IResult<TValue> {
public TValue Value { get; }
public SuccessResult(TValue value) { Value = value; }
public T Match<T>(Func<SuccessResult<TValue>, T> success,
Func<ErrorResult<TValue>, T> error) => success(this);
}
public class ErrorResult<TValue> : IResult<TValue>
{
public string Message { get; }
public ErrorResult(string message) { Message = message; }
public T Match<T>(Func<SuccessResult<TValue>, T> success,
Func<ErrorResult<TValue>, T> error) => error(this);
}
57. Pattern Matching(ish)
IResult<string> result = ParseIt();
string message = result.Match(
success => $"SUCCESS: ${success.Value}",
error => $"ERR: {error.Message}");
Console.WriteLine(message);
IResult result = ParseIt();
if (result is SuccessResult<string>) {
var success = (SuccessResult<string>)result;
Console.WriteLine($"SUCCESS: {success.Value}");
} else if (result is ErrorResult) {
var error = (ErrorResult)result;
Console.WriteLine($"ERR: {error.Message}");
}
58. The Match
Method
Forces us to handle all cases
Gives us an object with only valid
properties for that state
59. The New IResult
public interface IResult<Token, TValue> {
T Match<T>(Func<FailResult<Token, TValue>, T> fail,
Func<SuccessResult<Token, TValue>, T> success);
}
public class FailResult<Token, TValue> : IResult<Token, TValue> {
public string Message { get; }
public FailResult(string message) { Message = message; }
public T Match<T>(Func<FailResult<Token, TValue>, T> fail,
Func<SuccessResult<Token, TValue>, T> success) => fail(this);
}
public class SuccessResult<Token, TValue> : IResult<Token, TValue> {
public TValue Value { get; }
public ISource<Token> Next { get; }
public SuccessResult(TValue value, ISource<Token> next) { Value = value; Next = next; }
public T Match<T>(Func<FailResult<Token, TValue>, T> fail,
Func<SuccessResult<Token, TValue>, T> success) => success(this);
}
60. ISource also Represents Two States
public interface ISource<Token> {
Token Current { get; }
bool HasMore { get; }
ISource<Token> Next();
}
Only valid when HasMore = true
61. The New ISource
public interface ISource<Token> {
T Match<T>(Func<EmtySource<Token>, T> empty,
Func<SourceWithMoreContent<Token>, T> hasMore);
}
public class EmtySource<Token> : ISource<Token> {
// No properties! No state! Let's just make it singleton.
EmtySource() { }
public static readonly EmtySource<Token> Instance = new EmtySource<Token>();
public T Match<T>(Func<EmtySource<Token>, T> empty,
Func<SourceWithMoreContent<Token>, T> hasMore) => empty(this);
}
public class SourceWithMoreContent<Token> : ISource<Token> {
readonly Func<ISource<Token>> getNext;
public SourceWithMoreContent(Token current, Func<ISource<Token>> getNext) { Current = current; this.getNext = getNext; }
public Token Current { get; set; }
public ISource<Token> Next() => getNext();
public T Match<T>(Func<EmtySource<Token>, T> empty,
Func<SourceWithMoreContent<Token>, T> hasMore) => hasMore(this);
}
62. Make a String Source
public static class StringSource {
public static ISource<char> Create(string value, int index = 0) {
if (index >= value.Length)
return EmtySource<char>.Instance;
return new SourceWithMoreContent<char>(value[index], () => Create(value, index + 1));
}
}
public static ISource<char> Create(string value, int index = 0)
=> index >= value.Length
? (ISource<char>)EmtySource<char>.Instance
: new SourceWithMoreContent<char>(value[index], () => Create(value, index + 1));
63. Char Matches... public abstract class CharMatches : IRule<char, char> {
protected abstract bool IsCharMatch(char c);
public Result<char, char> TryParse(ISource<char> source) {
if (!source.HasMore)
return new Result<char, char>(false, '0', "Unexpected EOF", null);
if (!IsCharMatch(source.Current))
return new Result<char, char>(false, '0', $"Unexpected char: {source.Current}", null);
return new Result<char, char>(true, source.Current, null, source.Next());
}
}
public abstract class CharMatches : IRule<char, char> {
protected abstract bool IsCharMatch(char c);
public IResult<char, char> TryParse(ISource<char> source) {
var result = source.Match(
empty => (IResult<char, char>)new FailResult<char, char>("Unexpected EOF"),
hasMore =>
{
if (!IsCharMatch(hasMore.Current))
return new FailResult<char, char>($"Unexpected char: {hasMore.Current}");
return new SuccessResult<char, char>(hasMore.Current, hasMore.Next());
});
return result;
}
}
public IResult<char, char> TryParse(ISource<char> source)
=> source.Match(
empty => new FailResult<char, char>("Unexpected EOF"),
hasMore => IsCharMatch(hasMore.Current)
? new SuccessResult<char, char>(hasMore.Current, hasMore.Next())
: (IResult<char, char>)new FailResult<char, char>($"Unexpected char: {hasMore.Current}")
);
64. Match Then...
public IResult<Token, TOut> TryParse(ISource<Token> source) {
var leftResult = leftRule.TryParse(source);
var finalResult = leftResult.Match(
leftFail => new FailResult<Token, TOut>(leftFail.Message),
leftSuccess => {
var rightResult = rightRule.TryParse(leftSuccess.Next);
var rightFinalResult = rightResult.Match(
rightFail => (IResult<Token, TOut>)new FailResult<Token, TOut>(rightFail.Message),
rightSuccess => {
var finalValue = Combine(leftSuccess.Value, rightSuccess.Value);
return new SuccessResult<Token, TOut>(finalValue, rightSuccess.Next);
});
return rightFinalResult;
});
return finalResult;
}
public Result<Token, TOut> TryParse(ISource<Token> source) {
var leftResult = leftRule.TryParse(source);
if (!leftResult.Success)
return new Result<Token, TOut>(false, default(TOut), leftResult.Message, null);
var rightResult = rightRule.TryParse(leftResult.Next);
if (!rightResult.Success)
return new Result<Token, TOut>(false, default(TOut), rightResult.Message, null);
var result = Combine(leftResult.Value, rightResult.Value);
return new Result<Token, TOut>(true, result, null, rightResult.Next);
}
public IResult<Token, TOut> TryParse(ISource<Token> source)
=> leftRule.TryParse(source).Match(
leftFail => new FailResult<Token, TOut>(leftFail.Message),
leftSuccess =>
rightRule.TryParse(leftSuccess.Next).Match(
rightFail => (IResult<Token, TOut>)new FailResult<Token, TOut>(rightFail.Message),
rightSuccess => new SuccessResult<Token, TOut>(Combine(leftSuccess.Value, rightSuccess.Value),
rightSuccess.Next)
)
);
65. Invert Rule (Not)
public Result<Token, Token> TryParse(ISource<Token> source) {
if (!source.HasMore)
return new Result<Token, Token>(false, default(Token), "Unexpected EOF", null);
var result = rule.TryParse(source);
if (result.Success)
return new Result<Token, Token>(false, default(Token), "Unexpected match", null);
return new Result<Token, Token>(true, source.Current, null, source.Next());
}
public IResult<Token, Token> TryParse(ISource<Token> source)
=> source.Match(
empty => new FailResult<Token, Token>("Unexpected EOF"),
current => rule.TryParse(current).Match(
fail => new SuccessResult<Token, Token>(current.Current, current.Next()),
success => (IResult<Token, Token>)new FailResult<Token, Token>("Unexpected match")
)
);
67. Let’s Be Honest
All these `new` objects are ugly.
var quote = new CharIs('"');
var slash = new CharIs('');
var escapedQuote = new MatchThenKeep<char, char,
char>(slash, quote);
var escapedSlash = new MatchThenKeep<char, char,
char>(slash, slash);
var notQuote = new Not<char, char>(quote);
var insideQuoteChar = new FirstMatch<char, char>(new[] {
(IRule<char, char>)escapedQuote,
escapedSlash,
notQuote
});
var insideQuote = new Many<char, char>(insideQuoteChar,
false);
var insideQuoteAsString = new JoinText(insideQuote);
var openQuote = new MatchThenKeep<char, char,
string>(quote, insideQuoteAsString);
var fullQuote = new MatchThenIgnore<char, string,
char>(openQuote, quote);
68. Also
Single method interfaces are lame*.
It’s effectively a delegate.
public interface IRule<Token, TValue> {
IResult<Token, TValue> TryParse(ISource<Token> source);
}
*In a non-scientific poll of people who agree with me, 100% of
respondents confirmed this statement. Do not question its validity.
70. A Rule is a Delegate is a Function
public interface IRule<Token, TValue> {
IResult<Token, TValue> TryParse(ISource<Token> source);
}
public delegate IResult<Token, TValue> Rule<Token, TValue>(ISource<Token> source);
71. Char Matches...
public abstract class CharMatches : IRule<char, char> {
protected abstract bool IsCharMatch(char c);
public IResult<char, char> TryParse(ISource<char> source)
=> source.Match(
empty => new FailResult<char, char>("Unexpected EOF"),
hasMore => IsCharMatch(hasMore.Current)
? new SuccessResult<char, char>(hasMore.Current, hasMore.Next())
: (IResult<char, char>)new FailResult<char, char>($"Unexpected char: {hasMore.Current}")
);
}
public static class Rules {
public static Rule<char, char> CharMatches(Func<char, bool> isMatch)
=> (source) => source.Match(
empty => new FailResult<char, char>("Unexpected EOF"),
hasMore => isMatch(hasMore.Current)
? new SuccessResult<char, char>(hasMore.Current, hasMore.Next())
: (IResult<char, char>)new FailResult<char, char>($"Unexpected char: {hasMore.Current}")
);
}
public static Rule<char, char> CharIsDigit() => CharMatches(char.IsDigit);
public static Rule<char, char> CharIs(char c) => CharMatches(x => x == c);
74. Example Usage
var quote = Rules.CharIs('"');
var slash = Rules.CharIs('');
var escapedQuote = Rules.MatchThenKeep(slash, quote);
var escapedSlash = slash.MatchThenKeep(slash);
75. The Original 2.0 Definition
var quote = new CharIs('"');
var slash = new CharIs('');
var escapedQuote = new MatchThenKeep<char, char, char>(slash, quote);
var escapedSlash = new MatchThenKeep<char, char, char>(slash, slash);
var notQuote = new Not<char, char>(quote);
var insideQuoteChar = new FirstMatch<char, char>(new[] {
(IRule<char, char>)escapedQuote,
escapedSlash,
notQuote
});
var insideQuote = new Many<char, char>(insideQuoteChar, false);
var insideQuoteAsString = new JoinText(insideQuote);
var openQuote = new MatchThenKeep<char, char, string>(quote,
insideQuoteAsString);
var fullQuote = new MatchThenIgnore<char, string, char>(openQuote, quote);
var source = new StringSource(raw);
string asQuote;
if (fullQuote.TryParse(source, out asQuote))
return asQuote;
source.Move(0);
int asInteger;
if (digitsAsInt.TryParse(source, out asInteger))
return asInteger;
return null;
var digit = new CharIsDigit();
var digits = new Many<char, char>(digit, true);
var digitsString = new JoinText(digits);
var digitsAsInt = new MapToInteger(digitsString);
76. The Updated 3.0 Definition
var quote = Rules.CharIs('"');
var slash = Rules.CharIs('');
var escapedQuote = slash.MatchThenKeep(quote);
var escapedSlash = slash.MatchThenKeep(slash);
var notQuote = quote.Not();
var fullQuote = quote
.MatchThenKeep(
Rules.FirstMatch(
escapedQuote,
escapedSlash,
notQuote
).Many().JoinText()
)
.MatchThenIgnore(quote);
var finalResult = Rules.FirstMatch(
fullQuote.MapTo(x => (object)x),
digit.MapTo(x => (object)x)
);
var source = StringSource.Create(raw);
return finalResult(source).Match(
fail => null,
success => success.Value
);
var integer = Rules.CharIsDigit()
.Many(true)
.JoinText()
.MapToInteger();
81. Is it a good idea?
public static Rule<Token, Token> Not<Token, TResult>(this Rule<Token, TResult> rule)
=> (source) => source.Match(
empty => new FailResult<Token, Token>("Unexpected EOF"),
current => rule(current).Match(
fail => new SuccessResult<Token, Token>(current.Current, current.Next()),
success => (IResult<Token, Token>)new FailResult<Token, Token>("Unexpected match")
)
);
public static Rule<Token, TOut> MapTo<Token, TIn, TOut>(this Rule<Token, TIn> rule, Func<TIn, TOut> convert)
=> (source) => rule(source).Match(
fail => (IResult<Token, TOut>)new FailResult<Token, TOut>(fail.Message),
success => new SuccessResult<Token, TOut>(convert(success.Value), success.Next)
);
public static Rule<char, string> JoinText(this Rule<char, char[]> rule)
=> MapTo(rule, (x) => new string(x));
public static Rule<char, int> MapToInteger(this Rule<char, string> rule)
=> MapTo(rule, (x) => int.Parse(x));
82. Limitations
“At Zombocom, the only limit…
is yourself.”
1. Makes a LOT of short-lived
objects (ISources, IResults).
2. As written currently, you will end
up with the entire thing in
memory.
3. Visual Studio’s Intellisense
struggles with nested lambdas.
4. Frequently requires casts to solve
type inference problems.
5. It’s not very C#.
84. Iterations
Iteration 1.0: Procedural
Iteration 2.0: Making Compositional
with OOP
Iteration 2.1: Immutability
Iteration 2.2: Discriminated Unions and
Pattern Matching
Iteration 3.0: Functions as First Class
Citizens