Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Functional concepts in C#

520 views

Published on

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.

Published in: Technology
  • DOWNLOAD FULL BOOKS INTO AVAILABLE FORMAT ......................................................................................................................... ......................................................................................................................... 1.DOWNLOAD FULL PDF EBOOK here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... 1.DOWNLOAD FULL EPUB Ebook here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... 1.DOWNLOAD FULL doc Ebook here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... 1.DOWNLOAD FULL PDF EBOOK here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... 1.DOWNLOAD FULL EPUB Ebook here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... 1.DOWNLOAD FULL doc Ebook here { https://tinyurl.com/y8nn3gmc } ......................................................................................................................... ......................................................................................................................... ......................................................................................................................... .............. Browse by Genre Available eBooks ......................................................................................................................... Art, Biography, Business, Chick Lit, Children's, Christian, Classics, Comics, Contemporary, Cookbooks, Crime, Ebooks, Fantasy, Fiction, Graphic Novels, Historical Fiction, History, Horror, Humor And Comedy, Manga, Memoir, Music, Mystery, Non Fiction, Paranormal, Philosophy, Poetry, Psychology, Religion, Romance, Science, Science Fiction, Self Help, Suspense, Spirituality, Sports, Thriller, Travel, Young Adult,
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

Functional concepts in C#

  1. 1. Functional Concepts in C# Or “Who the F# Wrote This?” https://github.com/mrdrbob/sd-code-camp-2016
  2. 2. Thanks Sponsors!
  3. 3. Let’s Manage Expectations!
  4. 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. 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. 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. 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. 8. Let’s Build a Parser! A highly simplified JSON-like syntax for strings and integers.
  9. 9. Integers One or more digits
  10. 10. Strings Starts & ends with double quote. Quotes can be escaped with slash. Slash can be escaped with slash. Can be empty.
  11. 11. Iteration 1.0
  12. 12. The IParser<TValue> Interface public interface IParser<TValue>  { bool  TryParse(string raw,  out TValue value); }
  13. 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. 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. 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. 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. 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
  18. 18. Iteration 2.0 Composition
  19. 19. [Picture of Legos Here]
  20. 20. One or More Times A Parser Built on Rules (Integer Parser) [0-9]
  21. 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. 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. 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. 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. 25. Creating a Rule public interface IRule<Token,  TResult>  { bool  TryParse(ISource<Token> source,  out TResult result); }
  26. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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);
  36. 36. A Comparison
  37. 37. A Comparison
  38. 38. A Comparison
  39. 39. What an Improvement!
  40. 40. A Comparison (just the definition)
  41. 41. Room for Improvement 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; } } Out parameter :( Managing the source’s index
  42. 42. Iteration 2.1 Immutability
  43. 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. 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. 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. 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. 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. 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. 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. 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()); } }
  51. 51. Getting Better, but...
  52. 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
  53. 53. Iteration 2.2 Discriminated Unions and Pattern Matching (sorta)
  54. 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. 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. 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. 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. 58. The Match Method Forces us to handle all cases Gives us an object with only valid properties for that state
  59. 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. 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. 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. 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. 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. 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. 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") ) );
  66. 66. That’s nice but...
  67. 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. 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.
  69. 69. Iteration 3.0 Functions as First Class Citizens
  70. 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. 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);
  72. 72. 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);
  73. 73. Not, MapTo, JoinText, MapToInteger 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));
  74. 74. Example Usage var  quote  = Rules.CharIs('"'); var  slash  = Rules.CharIs(''); var  escapedQuote  =  Rules.MatchThenKeep(slash,  quote); var  escapedSlash  = slash.MatchThenKeep(slash);
  75. 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. 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();
  77. 77. A Comparison V1 -> V3
  78. 78. A Comparison V1 -> V3
  79. 79. A Comparison V1 -> V3 (Just Definition)
  80. 80. Looks Great! My co-workers are going to kill me
  81. 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. 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#.
  83. 83. Let’s Review
  84. 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
  85. 85. That’s All Thanks for coming and staying awake!

×