This repository has been archived by the owner on Apr 5, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #11 from wayfair/refinement-types
Refinement types
- Loading branch information
Showing
11 changed files
with
838 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
66 changes: 66 additions & 0 deletions
66
Example/Prelude.playground/Pages/reducers.xcplaygroundpage/Contents.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// | ||
// This source file is part of Prelude, an open source project by Wayfair | ||
// | ||
// Copyright (c) 2018 Wayfair, LLC. | ||
// Licensed under the 2-Clause BSD License | ||
// | ||
// See LICENSE.md for license information | ||
// | ||
|
||
import Prelude | ||
|
||
// | ||
// Reducers | ||
// | ||
|
||
// reducers have an `inout` “state” by default | ||
let reducer: Reducer<[String], String> = .init { arr, item in arr.append(item) } | ||
|
||
// but can also be written with an immutable state | ||
let reduceCaps: Reducer<[String], String> = .nextPartialResult { arr, item in | ||
return arr + [item.uppercased()] | ||
} | ||
|
||
// combine reducers with `<>` to execute them in sequence | ||
let bigReducer = reducer <> reduceCaps | ||
["foo", "bar", "baz"].reduce([], bigReducer) | ||
|
||
struct Person { var firstName, lastName: String } | ||
|
||
// use `pullback` to make it so existing reducers can chomp other types of values | ||
let people = [ | ||
Person(firstName: "foo", lastName: "bar"), | ||
Person(firstName: "baz", lastName: "qux") | ||
] | ||
let reduceFirstNames = bigReducer.pullback { (person: Person) in person.firstName } | ||
people.reduce(into: [], reduceFirstNames) | ||
|
||
// | ||
// Use reducers and `Changeable` together! | ||
// | ||
|
||
/// update a `Person` by taking the first component of a tuple as the new first name | ||
let processFirstName: Reducer<Changeable<Person>, (String, String)> = .init { person, tuple in | ||
let (newValue, _) = tuple | ||
person.write(newValue, at: \.firstName) | ||
} | ||
|
||
/// update a `Person` by taking the second component of a tuple as the new last name | ||
let processLastName: Reducer<Changeable<Person>, (String, String)> = .init { person, tuple in | ||
let (_, newValue) = tuple | ||
person.write(newValue, at: \.lastName) | ||
} | ||
|
||
let redundantChanges = [("foo", "bar"), ("foo", "bar")] | ||
redundantChanges.reduce( | ||
into: pure(Person(firstName: "foo", lastName: "bar")), | ||
processFirstName <> processLastName | ||
) | ||
.hasChanged // => false | ||
|
||
let nonRedundantChanges = [("foo", "bar"), ("foo", "qux")] | ||
nonRedundantChanges.reduce( | ||
into: pure(Person(firstName: "foo", lastName: "bar")), | ||
processFirstName <> processLastName | ||
) | ||
.hasChanged // => true |
95 changes: 95 additions & 0 deletions
95
Example/Prelude.playground/Pages/refinement-types.xcplaygroundpage/Contents.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
// | ||
// This source file is part of Prelude, an open source project by Wayfair | ||
// | ||
// Copyright (c) 2018 Wayfair, LLC. | ||
// Licensed under the 2-Clause BSD License | ||
// | ||
// See LICENSE.md for license information | ||
// | ||
|
||
import Prelude | ||
|
||
// | ||
// Refinement types | ||
// | ||
|
||
// let’s create a very basic `Person` type | ||
struct Person { var firstName, lastName: String } | ||
|
||
// there are some values of this type (eg. both names empty, only a defined `firstName`, etc.) that we probably want to consider invalid data or garbage in some way | ||
|
||
// we’ll write a `Refinement` to `Person` that expresses the fact that the names can’t be empty | ||
enum ValidName: Refinement { | ||
typealias BaseType = Person | ||
static func isValid(_ value: Person) -> Bool { | ||
return !value.firstName.isEmpty && !value.lastName.isEmpty | ||
} | ||
} | ||
|
||
// now, let’s put our refinement “rule” and the `Person` type itself into a box to create a new type that enforces our rules: | ||
typealias ValidPerson = Refined<Person, ValidName> | ||
|
||
// here’s a value of type `Person`. Let’s pretend we got it from somewhere else in our application, so we’re not sure whether it’s valid or not… | ||
let person = Person(firstName: "", lastName: "foo") | ||
|
||
// the `ValidPerson` initializer will only successfully `init` if the rule is satisfied: | ||
try? ValidPerson.init(person) // => nil | ||
|
||
try? ValidPerson.init(Person(firstName: "a", lastName: "b")) // => ok | ||
|
||
// let’s pretend we have a function that does Important Business Logic with persons | ||
func doImportantBusinessLogic(with person: Person) { | ||
// ensure the person is valid | ||
if person.firstName.isEmpty || person.lastName.isEmpty { | ||
fatalError("Person data was invalid! I can’t do my important thing") | ||
} | ||
// do important thing with a valid person here | ||
} | ||
|
||
// we can now refactor this function and the compiler will enforce our rule for us (no more `fatalError` needed)! | ||
func doImportantBusinessLogic2(with person: ValidPerson) { | ||
// names are guaranteed to be valid; do important thing with person here | ||
} | ||
|
||
// just to be clear about that: the following line does not compile… | ||
//doImportantBusinessLogic2(with: person) // => error: cannot convert value of type 'Person' to expected argument type 'ValidPerson' (aka 'Refined<Person, ValidName>') | ||
|
||
// we’ve taken what was a runtime error (`fatalError`), and made it into a compile-time error! | ||
|
||
// in a real app, this means fewer “Oops!” boxes, less bailing out of functions when data isn’t right, and less need to document invariants in code comments. Those things go away and are replaced with checks in the type system. | ||
|
||
// there’s a problem though. Our rule doesn’t hold for all possible users of our application. What about people who legitimately only have one name? We need another refinement: | ||
enum PersonIsPrince: Refinement { | ||
typealias BaseType = Person | ||
static func isValid(_ value: Person) -> Bool { | ||
return value.firstName == "Prince" && value.lastName.isEmpty // RIP | ||
} | ||
} | ||
|
||
// as long as the `BaseType`s match, we can compose refinements on the fly. The `OneOf` wrapper type creates a refinement that will let a value pass if either the left- **or** right-side refinement passes: | ||
typealias ValidName2 = OneOf<ValidName, PersonIsPrince> | ||
|
||
// now Prince can order a couch again. I won’t bother to write another typealias, here’s the full type inline: | ||
func doImportantBusinessLogic3(with person: Refined<Person, ValidName2>) { | ||
// I hope some purple couches are in stock | ||
} | ||
|
||
// here are some of the basic refinements that are already in the library | ||
Int.GreaterThanZero.of(1) // => ok | ||
|
||
Int.GreaterThan<One>.of(-99) // => nil | ||
|
||
String.NonEmpty.of("foo") // => ok | ||
|
||
String.NonEmpty.of("") // => nil | ||
|
||
// conditional conformance of refined types works as expected | ||
Int.GreaterThanZero.of(1) == Int.GreaterThanZero.of(1) // => true | ||
|
||
// to `compactMap` **and** refine at the same time, use `refineMap` | ||
let bar = [-1, 0, 1, 2, 3].refineMap(Int.GreaterThanZero.self) // => (3 values) | ||
|
||
// you can also write it like this | ||
let foo: [Refined<Int, Int.GreaterThanZero>] = [-1, 0, 1, 2, 3].refineMap() | ||
|
||
foo == bar // => true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,8 @@ | ||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | ||
<playground version='5.0' target-platform='ios' executeOnSourceChanges='false'> | ||
<timeline fileName='timeline.xctimeline'/> | ||
<playground version='6.0' target-platform='ios' executeOnSourceChanges='false'> | ||
<pages> | ||
<page name='change-tracking'/> | ||
<page name='reducers'/> | ||
<page name='refinement-types'/> | ||
</pages> | ||
</playground> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// | ||
// This source file is part of Prelude, an open source project by Wayfair | ||
// | ||
// Copyright (c) 2018 Wayfair, LLC. | ||
// Licensed under the 2-Clause BSD License | ||
// | ||
// See LICENSE.md for license information | ||
// | ||
|
||
public protocol Nat { | ||
static var intValue: Int { get } | ||
} | ||
|
||
public enum Succ<N: Nat>: Nat { | ||
public static var intValue: Int { return N.intValue + 1 } | ||
} | ||
|
||
public enum Zero: Nat { | ||
public static var intValue: Int { return 0 } | ||
} | ||
|
||
public typealias One = Succ<Zero> | ||
|
||
public typealias Two = Succ<Succ<Zero>> |
Oops, something went wrong.