Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Lens and Projections. #121

Merged
merged 2 commits into from Oct 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
51 changes: 51 additions & 0 deletions Sources/PenguinStructures/Lens.swift
@@ -0,0 +1,51 @@
//******************************************************************************
// Copyright 2020 Penguin Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

/// `KeyPath`s with a statically known `Value` endpoint.
///
/// This protocol allows us to create the constraint that some `Lens`'s `Focus` is-an instance of
/// `KeyPath`. For example:
///
/// public struct X<T, L: Lens> where L.Focus: KeyPath<T, L.Value> { ... }
/// ^^^^^
///
public protocol KeyPathProtocol: AnyKeyPath {
dabrahams marked this conversation as resolved.
Show resolved Hide resolved
/// The `KeyPath`'s focal point.
associatedtype Value
}

extension KeyPath: KeyPathProtocol {}

/// Types that represent, in the type system, a specific key path value.
///
/// A given `Lens`-conforming type's associated key path value is provided by its `static var
/// focus`.
public protocol Lens {
/// The specific subclass of `KeyPath<Focus.Root,Value>` whose value `Self` represents.
///
/// For example, `Focus` might be `WritableKeyPath<(Int, String), Int>` in a `Lens` that supported
/// writing.
associatedtype Focus: KeyPathProtocol

/// The `Value` type of the represented key path.
///
/// Models of `Lens` should not define this type, but instead allow the default to take effect.
associatedtype Value = Focus.Value

/// The key path value represented by `Self`.
static var focus: Focus { get }
}

169 changes: 169 additions & 0 deletions Sources/PenguinStructures/Projections.swift
@@ -0,0 +1,169 @@
//******************************************************************************
// Copyright 2020 Penguin Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

/// A Sequence whose elements are projections, through a lens of type `BaseElementPart`, of the
/// elements of some `Base` sequence.
///
/// `Projections` is analogous to `LazyMapSequence` but doesn't store a closure or a `KeyPath`, and
/// when `Base` conforms to `MutableCollection`, so does `Projections<Base, L>`, as long as
/// `L.Focus` is-a `WritableKeyPath`.
public struct Projections<Base: Sequence, BaseElementPart: Lens>
where BaseElementPart.Focus: KeyPath<Base.Element, BaseElementPart.Value>
{
/// The base sequence whose elements are being projected.
var base: Base
}

extension Projections: Sequence {
/// The type of each element of `self`.
public typealias Element = BaseElementPart.Value

/// Single-pass iteration interface and state for instances of `Self`.
public struct Iterator: IteratorProtocol {
/// An iterator over the `Base` elements
var base: Base.Iterator

/// Advances to and returns the next element, or returns `nil` if no next element exists.
public mutating func next() -> Element? {
base.next()?[keyPath: BaseElementPart.focus]
}
}

/// Returns an iterator over the elements of this sequence.
public func makeIterator() -> Iterator { Iterator(base: base.makeIterator()) }

/// A value <= `self.count`.
public var underestimatedCount: Int {
base.underestimatedCount
}
}

extension Projections: Collection where Base: Collection {
/// A position in an instance of `Self`.
public typealias Index = Base.Index

/// The indices of the elements in an instance of `self`.
public typealias Indices = Base.Indices

/// The position of the first element, or `endIndex` if `self.isEmpty`.
public var startIndex: Index { base.startIndex }

/// The position immediately after the last element.
public var endIndex: Index { base.endIndex }

/// The indices of all elements, in order.
public var indices: Indices { base.indices }

/// The position following `x`.
public func index(after x: Index) -> Index {
base.index(after: x)
}

/// Replaces `x` with its successor
public func formIndex(after x: inout Index) {
base.formIndex(after: &x)
}

/// Accesses the element at `i`.
public subscript(i: Index) -> Element {
get { base[i][keyPath: BaseElementPart.focus] }
}

/// True iff `self` contains no elements.
public var isEmpty: Bool { base.isEmpty }

/// The number of elements in `self`.
///
/// - Complexity: O(1) if `Base` conforms to `RandomAccessCollection`; otherwise, O(N) where N is
/// the number of elements.
public var count: Int { base.count }

/// Returns an index that is the specified distance from the given index.
///
/// - Complexity: O(1) if `Base` conforms to `RandomAccessCollection`; otherwise, O(`distance`).
public func index(_ i: Index, offsetBy distance: Int) -> Index {
base.index(i, offsetBy: distance)
}

/// Returns `i` offset forward by `distance`, unless that distance is beyond a given limiting
/// index, in which case nil is returned.
///
/// - Complexity: O(1) if `Base` conforms to `RandomAccessCollection`; otherwise, O(`distance`).
public func index(
_ i: Index, offsetBy distance: Int, limitedBy limit: Index
) -> Index? {
base.index(i, offsetBy: distance, limitedBy: limit)
}

/// Returns the number of positions between `start` to `end`.
///
/// - Complexity: O(1) if `Base` conforms to `RandomAccessCollection`; otherwise, worst case
/// O(`count`).
public func distance(from start: Index, to end: Index) -> Int {
base.distance(from: start, to: end)
}
}

extension Projections: MutableCollection
where Base: MutableCollection,
BaseElementPart.Focus : WritableKeyPath<Base.Element, BaseElementPart.Value>
{
/// Accesses the element at `i`.
public subscript(i: Index) -> Element {
get { base[i][keyPath: BaseElementPart.focus] }
set { base[i][keyPath: BaseElementPart.focus] = newValue }
_modify { yield &base[i][keyPath: BaseElementPart.focus] }
}
}

extension Projections: BidirectionalCollection
where Base: BidirectionalCollection
{
/// Returns the position immediately before `i`.
public func index(before i: Index) -> Index {
return base.index(before: i)
}

/// Replaces the value of `i` with its predecessor.
public func formIndex(before i: inout Index) {
return base.formIndex(before: &i)
}
}

extension Projections: RandomAccessCollection
where Base: RandomAccessCollection
{}

extension Sequence {
/// Accesses a sequence consisting of the elements of `self` projected through the given lens.
public subscript<L: Lens>(lens _: Type<L>) -> Projections<Self, L>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is subscript the right expression of this approach? (Concretely, collection transformations like map are functions instead of subscripts. Perhaps subscript only makes sense for the mutable collection where there's write-back?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subscript is certainly required for the mutable collection where there's write-back. There's just no alternative. Given that we have both read-only and read-write subscripts, I'd think giving the mutable and immutable cases nonuniform syntax would require some significant justification, no?

where L.Focus: KeyPath<Element, L.Value>
{
.init(base: self)
}
}

extension MutableCollection {
/// Accesses a sequence consisting of the elements of `self` projected through the given lens.
public subscript<L: Lens>(lens _: Type<L>) -> Projections<Self, L> {
get { .init(base: self) }
_modify {
var r = Projections<Self, L>(base: self)
defer { self = r.base }
yield &r
}
}
}
89 changes: 89 additions & 0 deletions Tests/PenguinStructuresTests/ProjectionTests.swift
@@ -0,0 +1,89 @@
//******************************************************************************
// Copyright 2020 Penguin Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
import PenguinStructures

struct Pair<T,U> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to use Tuple2?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't think of it. Hindsight justification: this is simpler, makes the test more self-contained, and allows us to extend Pair for testing without interacting with any other tests. That said, please LMK if you want me to change it.

var first: T
var second: U
}

extension Pair: Equatable where T: Equatable, U: Equatable {}

struct Project1st<T,U>: Lens {
static var focus: KeyPath<Pair<T, U>, T> { \.first }
}

struct MutablyProject2nd<T,U>: Lens {
static var focus: WritableKeyPath<Pair<T, U>, U> { \.second }
}

fileprivate func pairs(_ x: Range<Int>) -> [Pair<Int,String>] {
x.map { Pair(first: $0, second: String($0)) }
}

class ProjectionTests: XCTestCase {
let base = pairs(0..<10)
let replacementStrings = (0..<10).lazy.map(String.init).reversed()

typealias P1 = Type<Project1st<Int, String>>
typealias P2 = Type<MutablyProject2nd<Int, String>>

func testSequenceSemantics() {
AnySequence(base)[lens: P1()].checkSequenceSemantics(expecting: 0..<10)
}

func testCollectionSemantics() {
AnyCollection(base)[lens: P1()].checkCollectionSemantics(expecting: 0..<10)
}

func testMutableCollectionSemantics() {
var checkit = base[lens: P2()]
checkit.checkMutableCollectionSemantics(writing: replacementStrings)
}

func testBidirectionalCollectionSemantics() {
AnyBidirectionalCollection(base)[lens: P1()]
.checkBidirectionalCollectionSemantics(expecting: 0..<10)
}

func testRandomAccessCollectionSemantics() {
base[lens: P1()].checkRandomAccessCollectionSemantics(expecting: 0..<10)
}

func testLensSubscriptModify() {
// get is covered in all the other tests.
var target = base
target[lens: P2()][0] += "suffix"
XCTAssertEqual(target.first, Pair(first: 0, second: "0suffix"), "Mutation didn't write back.")
XCTAssertEqual(target[1...], base[1...], "unexpected mutation")

// Wholesale replacement through the projection replaces the whole target.
let base2 = pairs(5..<10)
target[lens: P2()] = base2[lens: P2()]
XCTAssertEqual(target, base2)
}

static var allTests = [
("testSequenceSemantics", testSequenceSemantics),
("testCollectionSemantics", testCollectionSemantics),
("testMutableCollectionSemantics", testMutableCollectionSemantics),
("testBidirectionalCollectionSemantics", testBidirectionalCollectionSemantics),
("testRandomAccessCollectionSemantics", testRandomAccessCollectionSemantics),
("testLensSubscriptModify", testLensSubscriptModify),
]
}