Functional concepts in C#

Post on 11-Apr-2017

181 views 2 download

Transcript of Functional concepts in C#

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!