Upload
blend-interactive
View
181
Download
2
Embed Size (px)
Citation preview
Functional Concepts in C#
Or “Who the F# Wrote This?”
https://github.com/mrdrbob/sd-code-camp-2016
Thanks Sponsors!
Let’s Manage Expectations!
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
What this talk isn’t
“OOP is dead!”
“Functional all the things!”
“All code should look exactly like this!” (Spoiler: it probably shouldn’t)
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
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
Let’s Build a Parser!
A highly simplified JSON-like syntax for strings and integers.
IntegersOne or more digits
StringsStarts & ends with double quote.Quotes can be escaped with slash.Slash can be escaped with slash.
Can be empty.
Iteration 1.0
The IParser<TValue> Interface
public interface IParser<TValue> {
bool TryParse(string raw, out TValue value);
}
IntegerParserpublic 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;
}
}
IntegerParserpublic 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;
}
}
StringParserpublic 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]);
elsereturn false;
} else {buffer.Add(raw[x]);
}
x += 1;}
if (x == raw.Length)return false;
x += 1;value = new string(buffer.ToArray());return true;
}}
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]);
elsereturn 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;
}
}
Rethinking the ParserMake 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
Iteration 2.0
Composition
[Picture of Legos Here]
One or More Times
A Parser Built on Rules (Integer Parser)
[0-9]
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
\ “
“
A Set of Rules
Match QuoteMatch SlashMatch Digit
Match Then KeepMatch Then IgnoreMatch AnyMatch Zero or More TimesMatch One or More TimesNot
Rethinking the SourceHandle 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
Rethinking the Sourcepublic 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;
}
Creating a Rulepublic interface IRule<Token, TResult> {
bool TryParse(ISource<Token> source, out TResult result);
}
Char Matches...public class CharIsQuote : IRule<char, char> {
public bool TryParse(ISource<char> source, outchar 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;
}}
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;
}
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;
}}
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;
}}
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;}
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!
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;
}}
Map Resultpublic 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;
}}
Map Resultpublic 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);
}
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);
A Comparison
A Comparison
A Comparison
What an Improvement!
A Comparison (just the definition)
Room for Improvementpublic 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;
}}
Out parameter :(
Managing the source’s index
Iteration 2.1
Immutability
An Immutable Sourcepublic 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();
}
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);
}
Ditch the Outpublic 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);
}
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());
}
}
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;
}
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);}
}
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);
}
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());
}}
Getting Better, but...
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
Iteration 2.2
Discriminated Unions and Pattern Matching (sorta)
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; }
}
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}");
}
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);}
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}");
}
The Match Method Forces us to handle all cases
Gives us an object with only valid properties for that state
The New IResultpublic 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);
}
ISource also Represents Two States
public interface ISource<Token> {
Token Current { get; }
bool HasMore { get; }
ISource<Token> Next();
}
Only valid when HasMore = true
The New ISourcepublic 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);
}
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));
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}")
);
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))
);
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")
)
);
That’s nice but...
Let’s Be HonestAll 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);
AlsoSingle 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.
Iteration 3.0
Functions as First Class Citizens
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);
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);
Then (Keep|Ignore)public static Rule<Token, TOut> MatchThen<Token, TLeft, TRight, TOut>(this Rule<Token, TLeft> leftRule, Rule<Token, TRight> rightRule, Func<TLeft, TRight, TOut> convert)
=> (source) => leftRule(source).Match(leftFail => new FailResult<Token, TOut>(leftFail.Message),leftSuccess =>
rightRule(leftSuccess.Next).Match(rightFail => (IResult<Token, TOut>)new FailResult<Token, TOut>(rightFail.Message),rightSuccess => new SuccessResult<Token, TOut>(convert(leftSuccess.Value, rightSuccess.Value),
rightSuccess.Next))
);
public static Rule<Token, TRight> MatchThenKeep<Token, TLeft, TRight>(this Rule<Token, TLeft> leftRule, Rule<Token, TRight> rightRule)
=> MatchThen(leftRule, rightRule, (left, right) => right);
public static Rule<Token, TLeft> MatchThenIgnore<Token, TLeft, TRight>(this Rule<Token, TLeft> leftRule, Rule<Token, TRight> rightRule)
=> MatchThen(leftRule, rightRule, (left, right) => left);
Not, MapTo, JoinText, MapToIntegerpublic 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));
Example Usage
var quote = Rules.CharIs('"');
var slash = Rules.CharIs('\\');
var escapedQuote = Rules.MatchThenKeep(slash, quote);
var escapedSlash = slash.MatchThenKeep(slash);
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);
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();
A Comparison V1 -> V3
A Comparison V1 -> V3
A Comparison V1 -> V3 (Just Definition)
Looks Great!My co-workers are going to kill me
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));
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#.
Let’s Review
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
That’s All
Thanks for coming and staying awake!