diff --git a/0061-composable-parsing-zip/Composable Parsers.playground/Contents.swift b/0061-composable-parsing-zip/Composable Parsers.playground/Contents.swift new file mode 100644 index 00000000..4d9ee3be --- /dev/null +++ b/0061-composable-parsing-zip/Composable Parsers.playground/Contents.swift @@ -0,0 +1,381 @@ +import Foundation + +struct Parser { + let run: (inout Substring) -> A? +} + +let int = Parser { str in + let prefix = str.prefix(while: { $0.isNumber }) + guard let int = Int(prefix) else { return nil } + str.removeFirst(prefix.count) + return int +} + +let double = Parser { str in + let prefix = str.prefix(while: { $0.isNumber || $0 == "." }) + guard let match = Double(prefix) else { return nil } + str.removeFirst(prefix.count) + return match +} + +func literal(_ literal: String) -> Parser { + return Parser { str in + guard str.hasPrefix(literal) else { return nil } + str.removeFirst(literal.count) + return () + } +} + +func always(_ a: A) -> Parser { + return Parser { _ in a } +} + +extension Parser { + static var never: Parser { + return Parser { _ in nil } + } +} + +struct Coordinate { + let latitude: Double + let longitude: Double +} + + +extension Parser { + func run(_ str: String) -> (match: A?, rest: Substring) { + var str = str[...] + let match = self.run(&str) + return (match, str) + } +} + + +// map: ((A) -> B) -> (F) -> F + +// F = Parser +// map: ((A) -> B) -> (Parser) -> Parser + +// map(id) = id + +[1, 2, 3] + .map { $0 } + +Optional("Blob") + .map { $0 } + + +// map: (Parser, (A) -> B) -> Parser + +extension Parser { + func map(_ f: @escaping (A) -> B) -> Parser { + return Parser { str -> B? in + self.run(&str).map(f) + } + } + + func fakeMap(_ f: @escaping (A) -> B) -> Parser { + return Parser { _ in nil } + } + func fakeMap2(_ f: @escaping (A) -> B) -> Parser { + return Parser { str in + let matchB = self.run(&str).map(f) + str = "" + return matchB + } + } +} + +int.map { $0 } +int.fakeMap { $0 }.run("123") +int + .fakeMap2 { $0 }.run("123 Hello World") +int + .run("123 Hello World") + +let even = int.map { $0 % 2 == 0 } + +even.run("123 Hello World") +even.run("42 Hello World") + +let char = Parser { str in + guard !str.isEmpty else { return nil } + return str.removeFirst() +} + +//let northSouth = Parser { str in +// guard +// let cardinal = str.first, +// cardinal == "N" || cardinal == "S" +// else { return nil } +// str.removeFirst(1) +// return cardinal == "N" ? 1 : -1 +//} + +// flatMap: ((A) -> M) -> (M) -> M + + +extension Parser { + func flatMap(_ f: @escaping (A) -> Parser) -> Parser { + return Parser { str -> B? in + let original = str + let matchA = self.run(&str) + let parserB = matchA.map(f) + guard let matchB = parserB?.run(&str) else { + str = original + return nil + } + return matchB + } + } +} + + +//let eastWest = Parser { str in +// guard +// let cardinal = str.first, +// cardinal == "E" || cardinal == "W" +// else { return nil } +// str.removeFirst(1) +// return cardinal == "E" ? 1 : -1 +//} + +func parseLatLong(_ str: String) -> Coordinate? { + var str = str[...] + + guard + let lat = double.run(&str), + literal("° ").run(&str) != nil, + let latSign = northSouth.run(&str), + literal(", ").run(&str) != nil, + let long = double.run(&str), + literal("° ").run(&str) != nil, + let longSign = eastWest.run(&str) + else { return nil } + + return Coordinate( + latitude: lat * latSign, + longitude: long * longSign + ) +} + +print(String(describing: parseLatLong("40.6782° N, 73.9442° W"))) + + +"40.6782° N, 73.9442° W" + +let coord = double + .flatMap { lat in + literal("° ") + .flatMap { _ in + northSouth + .flatMap { latSign in + literal(", ") + .flatMap { _ in + double + .flatMap { long in + literal("° ") + .flatMap { _ in + eastWest + .map { longSign in + return Coordinate( + latitude: lat * latSign, + longitude: long * longSign + ) + } + } + } + } + } + } +} + +coord.run("40.6782° N, 73.9442° W") +coord.run("40.6782° Z, 73.9442° W") + + +// zip: (F, F) -> F<(A, B)> + +func zip(_ a: Parser, _ b: Parser) -> Parser<(A, B)> { + return Parser<(A, B)> { str -> (A, B)? in + let original = str + guard let matchA = a.run(&str) else { return nil } + guard let matchB = b.run(&str) else { + str = original + return nil + } + return (matchA, matchB) + } +} + + +"$10" +"€10" + + +enum Currency { + case eur + case gbp + case usd +} + +let currency = char.flatMap { + $0 == "€" ? always(Currency.eur) + : $0 == "£" ? always(.gbp) + : $0 == "$" ? always(.usd) + : .never +} + + +struct Money { + let currency: Currency + let value: Double +} + +let money = zip(currency, double).map(Money.init) +money.run("$10") +money.run("£10") +money.run("€10") +money.run("฿10") + +"40.6782° N, 73.9442° W" + +zip(zip(double, literal("° ")), northSouth) + +func zip( + _ a: Parser, + _ b: Parser, + _ c: Parser + ) -> Parser<(A, B, C)> { + return zip(a, zip(b, c)) + .map { a, bc in (a, bc.0, bc.1) } +} + +zip(double, literal("° "), northSouth) + +zip(zip(double, literal("° "), northSouth), literal(", ")) + +func zip( + _ a: Parser, + _ b: Parser, + _ c: Parser, + _ d: Parser + ) -> Parser<(A, B, C, D)> { + return zip(a, zip(b, c, d)) + .map { a, bcd in (a, bcd.0, bcd.1, bcd.2) } +} + +zip(double, literal("° "), northSouth, literal(", ")) + + +func zip( + _ a: Parser, + _ b: Parser, + _ c: Parser, + _ d: Parser, + _ e: Parser + ) -> Parser<(A, B, C, D, E)> { + + return zip(a, zip(b, c, d, e)) + .map { a, bcde in (a, bcde.0, bcde.1, bcde.2, bcde.3) } +} +func zip( + _ a: Parser, + _ b: Parser, + _ c: Parser, + _ d: Parser, + _ e: Parser, + _ f: Parser + ) -> Parser<(A, B, C, D, E, F)> { + + return zip(a, zip(b, c, d, e, f)) + .map { a, bcdef in (a, bcdef.0, bcdef.1, bcdef.2, bcdef.3, bcdef.4) } +} +func zip( + _ a: Parser, + _ b: Parser, + _ c: Parser, + _ d: Parser, + _ e: Parser, + _ f: Parser, + _ g: Parser + ) -> Parser<(A, B, C, D, E, F, G)> { + + return zip(a, zip(b, c, d, e, f, g)) + .map { a, bcdefg in (a, bcdefg.0, bcdefg.1, bcdefg.2, bcdefg.3, bcdefg.4, bcdefg.5) } +} + +"40.6782° N, 73.9442° W" + + + +func parseLatLongHandRolled(_ string: String) -> Coordinate? { + let parts = string.split(separator: " ") + guard parts.count == 4 else { return nil } + guard + let lat = Double(parts[0].dropLast()), + let long = Double(parts[2].dropLast()) + else { return nil } + let latCard = parts[1].dropLast() + guard latCard == "N" || latCard == "S" else { return nil } + let longCard = parts[3] + guard longCard == "E" || longCard == "W" else { return nil } + let latSign = latCard == "N" ? 1.0 : -1 + let longSign = longCard == "E" ? 1.0 : -1 + return Coordinate(latitude: lat * latSign, longitude: long * longSign) +} + +parseLatLongHandRolled("40.446° N, 79.982° W") + + + +func parseLatLongWithScanner(_ string: String) -> Coordinate? { + let scanner = Scanner(string: string) + + var lat: Double = 0 + var northSouth: NSString? = "" + var long: Double = 0 + var eastWest: NSString? = "" + + guard + scanner.scanDouble(&lat), + scanner.scanString("° ", into: nil), + scanner.scanCharacters(from: ["N", "S"], into: &northSouth), + scanner.scanString(", ", into: nil), + scanner.scanDouble(&long), + scanner.scanString("° ", into: nil), + scanner.scanCharacters(from: ["E", "W"], into: &eastWest) + else { return nil } + + let latSign = northSouth == "N" ? 1.0 : -1 + let longSign = eastWest == "E" ? 1.0 : -1 + + return Coordinate(latitude: lat * latSign, longitude: long * longSign) +} + +parseLatLongWithScanner("40.446° N, 79.982° W") + +let northSouth = char + .flatMap { + $0 == "N" ? always(1.0) + : $0 == "S" ? always(-1) + : .never +} +let eastWest = char + .flatMap { + $0 == "E" ? always(1.0) + : $0 == "W" ? always(-1) + : .never +} +let latitude = zip(double, literal("° "), northSouth) + .map { lat, _, latSign in lat * latSign } +let longitude = zip(double, literal("° "), eastWest) + .map { long, _, longSign in long * longSign } +let coord2 = zip(latitude, literal(", "), longitude) + .map { lat, _, long in + Coordinate( + latitude: lat, + longitude: long + ) +} + diff --git a/0061-composable-parsing-zip/Composable Parsers.playground/contents.xcplayground b/0061-composable-parsing-zip/Composable Parsers.playground/contents.xcplayground new file mode 100644 index 00000000..63b6dd8d --- /dev/null +++ b/0061-composable-parsing-zip/Composable Parsers.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/0061-composable-parsing-zip/README.md b/0061-composable-parsing-zip/README.md new file mode 100644 index 00000000..9ee64ccc --- /dev/null +++ b/0061-composable-parsing-zip/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [Composable Parsing: Zip](https://www.pointfree.co/episodes/ep61-composable-parsing-zip) +> +> While `flatMap` allowed us to take our parser type to the next level, it introduced a nesting problem. Isn't `flatMap` all about solving nesting problems!? Well, we have one more operation at our disposal: `zip`! Let's define `zip` on the parser type, see what it brings to the table, and finally ask, "what's the point?" diff --git a/README.md b/README.md index afbaa091..a403d93b 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,4 @@ This repository is the home of code written on episodes of 1. [What Is a Parser?: Part 3](0058-what-is-a-parser-pt3) 1. [Composable Parsing: Map](0059-composable-parsing-map) 1. [Composable Parsing: Flat-Map](0060-composable-parsing-flat-map) +1. [Composable Parsing: Zip](0061-composable-parsing-zip)