Skip to content
This repository has been archived by the owner on Apr 5, 2022. It is now read-only.

Commit

Permalink
Merge pull request #11 from wayfair/refinement-types
Browse files Browse the repository at this point in the history
Refinement types
  • Loading branch information
Peter Tomaselli authored Mar 11, 2019
2 parents 2d6c5ee + d842ed6 commit 615fdd4
Show file tree
Hide file tree
Showing 11 changed files with 838 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
//
// 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

//
Expand Down Expand Up @@ -72,57 +81,3 @@ let anotherPerson = pure(curry(Person.init))
<*> Changeable(hasChanged: false, value: "foo")
<*> Changeable(hasChanged: false, value: "bar")
anotherPerson.hasChanged // => false

//
// 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)

// use `contramap` 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 both 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
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
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
8 changes: 6 additions & 2 deletions Example/Prelude.playground/contents.xcplayground
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>
32 changes: 31 additions & 1 deletion Prelude.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@
/* End PBXAggregateTarget section */

/* Begin PBXBuildFile section */
244559F92229775A00CFC0EB /* RefinementsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 244559F82229775A00CFC0EB /* RefinementsTests.swift */; };
244559FA2229775A00CFC0EB /* RefinementsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 244559F82229775A00CFC0EB /* RefinementsTests.swift */; };
249A08412226F8860060AE5D /* Refinements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 249A08402226F8860060AE5D /* Refinements.swift */; };
249A08422226F8860060AE5D /* Refinements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 249A08402226F8860060AE5D /* Refinements.swift */; };
249EE629222DC02200F93941 /* Nat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 249EE628222DC02200F93941 /* Nat.swift */; };
249EE62A222DC02200F93941 /* Nat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 249EE628222DC02200F93941 /* Nat.swift */; };
24F05CFF223074E3003502D0 /* Predicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F05CFE223074E3003502D0 /* Predicate.swift */; };
24F05D00223074E3003502D0 /* Predicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F05CFE223074E3003502D0 /* Predicate.swift */; };
24F05D022231581F003502D0 /* PredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F05D012231581F003502D0 /* PredicateTests.swift */; };
24F05D032231581F003502D0 /* PredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F05D012231581F003502D0 /* PredicateTests.swift */; };
66541CC5217F6698001E088D /* Prelude.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66541CBB217F6698001E088D /* Prelude.framework */; };
6657D9B121AFA8960065E051 /* ChangeTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FE1E1121AC788200DF98C0 /* ChangeTracking.swift */; };
6657D9B221AFA8960065E051 /* Monoid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FE1E0E21AC788200DF98C0 /* Monoid.swift */; };
Expand Down Expand Up @@ -65,6 +75,11 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
244559F82229775A00CFC0EB /* RefinementsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefinementsTests.swift; sourceTree = "<group>"; };
249A08402226F8860060AE5D /* Refinements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Refinements.swift; sourceTree = "<group>"; };
249EE628222DC02200F93941 /* Nat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nat.swift; sourceTree = "<group>"; };
24F05CFE223074E3003502D0 /* Predicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Predicate.swift; sourceTree = "<group>"; };
24F05D012231581F003502D0 /* PredicateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PredicateTests.swift; sourceTree = "<group>"; };
66541CBB217F6698001E088D /* Prelude.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Prelude.framework; sourceTree = BUILT_PRODUCTS_DIR; };
66541CC4217F6698001E088D /* PreludeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PreludeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
6657D9A721AFA0B80065E051 /* Prelude.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Prelude.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -151,11 +166,13 @@
66541CC8217F6698001E088D /* PreludeTests */ = {
isa = PBXGroup;
children = (
79E4CF6C21801D4800273142 /* Supporting Files */,
66FE1E1C21AC78A900DF98C0 /* ChangeTrackingTests.swift */,
24F05D012231581F003502D0 /* PredicateTests.swift */,
66FE1E1E21AC78A900DF98C0 /* PreludeTests.swift */,
66FE1E1F21AC78A900DF98C0 /* ReducersTests.swift */,
244559F82229775A00CFC0EB /* RefinementsTests.swift */,
66FE1E1D21AC78A900DF98C0 /* SequenceExtensionsTests.swift */,
79E4CF6C21801D4800273142 /* Supporting Files */,
);
path = PreludeTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -212,10 +229,13 @@
children = (
66FE1E1121AC788200DF98C0 /* ChangeTracking.swift */,
66FE1E0E21AC788200DF98C0 /* Monoid.swift */,
249EE628222DC02200F93941 /* Nat.swift */,
66FE1E1021AC788200DF98C0 /* Operators.swift */,
24F05CFE223074E3003502D0 /* Predicate.swift */,
66FE1E1321AC788200DF98C0 /* Prelude.h */,
66FE1E0F21AC788200DF98C0 /* Prelude.swift */,
66FE1E1221AC788200DF98C0 /* Reducers.swift */,
249A08402226F8860060AE5D /* Refinements.swift */,
66FE1E1421AC788200DF98C0 /* Sequence+Prelude.swift */,
);
name = Prelude;
Expand Down Expand Up @@ -427,7 +447,10 @@
66FE1E1B21AC788200DF98C0 /* Sequence+Prelude.swift in Sources */,
66FE1E1521AC788200DF98C0 /* Monoid.swift in Sources */,
66FE1E1621AC788200DF98C0 /* Prelude.swift in Sources */,
249A08412226F8860060AE5D /* Refinements.swift in Sources */,
66FE1E1721AC788200DF98C0 /* Operators.swift in Sources */,
24F05CFF223074E3003502D0 /* Predicate.swift in Sources */,
249EE629222DC02200F93941 /* Nat.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -438,7 +461,9 @@
66FE1E2121AC78A900DF98C0 /* SequenceExtensionsTests.swift in Sources */,
66FE1E2021AC78A900DF98C0 /* ChangeTrackingTests.swift in Sources */,
66FE1E2321AC78A900DF98C0 /* ReducersTests.swift in Sources */,
244559F92229775A00CFC0EB /* RefinementsTests.swift in Sources */,
66FE1E2221AC78A900DF98C0 /* PreludeTests.swift in Sources */,
24F05D022231581F003502D0 /* PredicateTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -451,7 +476,10 @@
6657D9B721AFA8960065E051 /* Sequence+Prelude.swift in Sources */,
6657D9B521AFA8960065E051 /* Prelude.swift in Sources */,
6657D9B221AFA8960065E051 /* Monoid.swift in Sources */,
249A08422226F8860060AE5D /* Refinements.swift in Sources */,
6657D9B321AFA8960065E051 /* Operators.swift in Sources */,
24F05D00223074E3003502D0 /* Predicate.swift in Sources */,
249EE62A222DC02200F93941 /* Nat.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -462,7 +490,9 @@
6657D9CA21AFA99B0065E051 /* SequenceExtensionsTests.swift in Sources */,
6657D9C921AFA99B0065E051 /* ReducersTests.swift in Sources */,
6657D9C721AFA99B0065E051 /* ChangeTrackingTests.swift in Sources */,
244559FA2229775A00CFC0EB /* RefinementsTests.swift in Sources */,
6657D9C821AFA99B0065E051 /* PreludeTests.swift in Sources */,
24F05D032231581F003502D0 /* PredicateTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
24 changes: 24 additions & 0 deletions Sources/Prelude/Nat.swift
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>>
Loading

0 comments on commit 615fdd4

Please sign in to comment.