Type-safe observable values and collections in Swift

Related tags

Code Quality GlueKit
Overview

GlueKit

Swift 3 License Platform

Build Status Code Coverage

Carthage compatible CocoaPod Version

⚠️ WARNING ⚠️ This project is in a prerelease state. There is active work going on that will result in API changes that can/will break code while things are finished. Use with caution.

GlueKit is a Swift framework for creating observables and manipulating them in interesting and useful ways. It is called GlueKit because it lets you stick stuff together.

GlueKit contains type-safe analogues for Cocoa's Key-Value Coding and Key-Value Observing subsystems, written in pure Swift. Besides providing the basic observation mechanism, GlueKit also supports full-blown key path observing, where a sequence of properties starting at a particular entity is observed at once. (E.g., you can observe a person's best friend's favorite color, which might change whenever the person gets a new best friend, or when the friend changes their mind about which color they like best.)

(Note though that GlueKit's keys are functions so they aren't as easy to serialize as KVC's string-based keys and key paths. It is definitely possible to implement serializable type-safe keys in Swift; but it involves some boilerplate code that's better handled by code generation or core language enhancements such as property behaviors or improved reflection capabilities.)

Like KVC/KVO, GlueKit supports observing not only individual values, but also collections like sets or arrays. This includes full support for key path observing, too -- e.g., you can observe a person's children's children as a single set. These observable collections report fine-grained incremental changes (e.g., "'foo' was inserted at index 5"), allowing you to efficiently react to their changes.

Beyond key path observing, GlueKit also provides a rich set of transformations and combinations for observables as a more flexible and extensible Swift version of KVC's collection operators. E.g., given an observable array of integers, you can (efficiently!) observe the sum of its elements; you can filter it for elements that match a particular predicate; you can get an observable concatenation of it with another observable array; and you can do much more.

You can use GlueKit's observable arrays to efficiently provide data to a UITableView or UICollectionView, including providing them with incremental changes for animated updates. This functionality is roughly equivalent to what NSFetchedResultsController does in Core Data.

GlueKit is written in pure Swift; it does not require the Objective-C runtime for its functionality. However, it does provide easy-to-use adapters that turn KVO-compatible key paths on NSObjects into GlueKit observables.

GlueKit hasn't been officially released yet. Its API is still in flux, and it has wildly outdated and woefully incomplete documentation. However, the project is getting close to a feature set that would make a coherent 1.0 version; I expect to have a useful first release before the end of 2016.

Presentation

Károly gave a talk on GlueKit during Functional Swift Conference 2016 in Budapest. Watch the video or read the slides.

Installation

CocoaPods

If you use CocoaPods, you can start using GlueKit by including it as a dependency in your Podfile:

pod 'GlueKit', :git => 'https://github.com/attaswift/GlueKit.git'

(There are no official releases of GlueKit yet; the API is incomplete and very unstable for now.)

Carthage

For Carthage, add the following line to your Cartfile:

github "attaswift/GlueKit" "<commit-hash>"

(You have to use a specific commit hash, because there are no official releases of GlueKit yet; the API is incomplete and very unstable for now.)

Swift Package Manager

For Swift Package Manager, add the following entry to the dependencies list inside your Package.swift file:

.Package(url: "https://github.com/attaswift/GlueKit.git", branch: master)

Standalone Development

If you don't use CocoaPods, Carthage or SPM, you need to clone GlueKit, BTree and SipHash, and add references to their xcodeproj files to your project's workspace. You may put the clones wherever you like, but if you use Git for your app development, it is a good idea to set them up as submodules of your app's top-level Git repository.

To link your application binary with GlueKit, just add GlueKit.framework, BTree.framework and SipHash.framework from the BTree project to the Embedded Binaries section of your app target's General page in Xcode. As long as the GlueKit and BTree project files are referenced in your workspace, these frameworks will be listed in the "Choose items to add" sheet that opens when you click on the "+" button of your target's Embedded Binaries list.

There is no need to do any additional setup beyond adding the framework targets to Embedded Binaries.

Working on GlueKit Itself

If you want to do some work on GlueKit on its own, without embedding it in an application, simply clone this repo with the --recursive option, open GlueKit.xcworkspace, and start hacking.

git clone --recursive https://github.com/attaswift/GlueKit.git GlueKit
open GlueKit/GlueKit.xcworkspace

Importing GlueKit

Once you've made GlueKit available in your project, you need to import it at the top of each .swift file in which you want to use its features:

import GlueKit

Similar frameworks

Some of GlueKit's constructs can be matched with those in discrete reactive frameworks, such as ReactiveCocoa, RxSwift, ReactKit, Interstellar, and others. Sometimes GlueKit even uses the same name for the same concept. But often it doesn't (sorry).

GlueKit concentrates on creating a useful model for observables, rather than trying to unify observable-like things with task-like things. GlueKit explicitly does not attempt to directly model networking operations (although a networking support library could certainly use GlueKit to implement some of its features). As such, GlueKit's source/signal/stream concept transmits simple values; it doesn't wrap them in Events.

I have several reasons I chose to create GlueKit instead of just using a better established and bug-free library:

  • I wanted to have some experience with reactive stuff, and you can learn a lot about a paradigm by trying to construct its foundations on your own. The idea is that I start simple and add things as I find I need them. I want to see if I arrive at the same problems and solutions as the Smart People who created the popular frameworks. Some common reactive patterns are not obviously right at first glance.
  • I wanted to experiment with reentrant observables, where an observer is allowed to trigger updates to the observable to which it's connected. I found no well-known implementation of Observable that gets this just right.
  • Building a library is a really fun diversion!

Overview

The GlueKit Overview describes the basic concepts of GlueKit.

Appetizer

Let's say you're writing a bug tracker application that has a list of projects, each with its own set of issues. With GlueKit, you'd use Variables to define your model's attributes and relationships:

class Project {
    let name: Variable<String>
    let issues: ArrayVariable<Issue>
}

class Account {
    let name: Variable<String>
    let email: Variable<String>
}

class Issue {
    let identifier: Variable<String>
    let owner: Variable<Account>
    let isOpen: Variable<Bool>
    let created: Variable<NSDate>
}

class Document {
    let accounts: ArrayVariable<Account>
    let projects: ArrayVariable<Project>
}

You can use a let observable: Variable<Foo> like you would a var raw: Foo property, except you need to write observable.value whenever you'd write raw:

// Raw Swift       ===>      // GlueKit                                    
var a = 42          ;        let b = Variable<Int>(42) 
print("a = \(a)")   ;        print("b = \(b.value\)")
a = 7               ;        b.value = 7

Given the model above, in Cocoa you could specify key paths for accessing various parts of the model from a Document instance. For example, to get the email addresses of all issue owners in one big unsorted array, you'd use the Cocoa key path "projects.issues.owner.email". GlueKit is able to do this too, although it uses a specially constructed Swift closure to represent the key path:

let cocoaKeyPath: String = "projects.issues.owner.email"

let swiftKeyPath: Document -> AnyObservableValue<[String]> = { document in 
    document.projects.flatMap{$0.issues}.flatMap{$0.owner}.map{$0.email} 
}

(The type declarations are included to make it clear that GlueKit is fully type-safe. Swift's type inference is able to find these out automatically, so typically you'd omit specifying types in declarations like this.) The GlueKit syntax is certainly much more verbose, but in exchange it is typesafe, much more flexible, and also extensible. Plus, there is a visual difference between selecting a single value (map) or a collection of values (flatMap), which alerts you that using this key path might be more expensive than usual. (GlueKit's key paths are really just combinations of observables. map is a combinator that is used to build one-to-one key paths; there are many other interesting combinators available.)

In Cocoa, you would get the current list of emails using KVC's accessor method. In GlueKit, if you give the key path a document instance, it returns an AnyObservableValue that has a value property that you can get.

let document: Document = ...
let cocoaEmails: AnyObject? = document.valueForKeyPath(cocoaKeyPath)
let swiftEmails: [String] = swiftKeyPath(document).value

In both cases, you get an array of strings. However, Cocoa returns it as an optional AnyObject that you'll need to unwrap and cast to the correct type yourself (you'll want to hold your nose while doing so). Boo! GlueKit knows what type the result is going to be, so it gives it to you straight. Yay!

Neither Cocoa nor GlueKit allows you to update the value at the end of this key path; however, with Cocoa, you only find this out at runtime, while with GlueKit, you get a nice compiler error:

// Cocoa: Compiles fine, but oops, crash at runtime
document.setValue("[email protected]", forKeyPath: cocoaKeyPath)
// GlueKit/Swift: error: cannot assign to property: 'value' is a get-only property
swiftKeyPath(document).value = "[email protected]"

You'll be happy to know that one-to-one key paths are assignable in both Cocoa and GlueKit:

let issue: Issue = ...
/* Cocoa */   issue.setValue("[email protected]", forKeyPath: "owner.email") // OK
/* GlueKit */ issue.owner.map{$0.email}.value = "[email protected]"  // OK

(In GlueKit, you generally just use the observable combinators directly instead of creating key path entities. So we're going to do that from now on. Serializable type-safe key paths require additional work, which is better provided by a potentional future model object framework built on top of GlueKit.)

More interestingly, you can ask to be notified whenever a key path changes its value.

// GlueKit
let c = document.projects.flatMap{$0.issues}.flatMap{$0.owner}.map{$0.name}.subscribe { emails in 
    print("Owners' email addresses are: \(emails)")
}
// Call c.disconnect() when you get bored of getting so many emails.

// Cocoa
class Foo {
    static let context: Int8 = 0
    let document: Document
    
    init(document: Document) {
        self.document = document
        document.addObserver(self, forKeyPath: "projects.issues.owner.email", options: .New, context:&context)
    }
    deinit {
        document.removeObserver(self, forKeyPath: "projects.issues.owner.email", context: &context)
    }
    func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, 
                                change change: [String : AnyObject]?, 
                                context context: UnsafeMutablePointer<Void>) {
        if context == &self.context {
	    print("Owners' email addresses are: \(change[NSKeyValueChangeNewKey]))
        }
        else {
            super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
        }
    }
}

Well, Cocoa is a mouthful, but people tend to wrap this up in their own abstractions. In both cases, a new set of emails is printed whenever the list of projects changes, or the list of issues belonging to any project changes, or the owner of any issue changes, or if the email address is changed on an individual account.

To present a more down-to-earth example, let's say you want to create a view model for a project summary screen that displays various useful data about the currently selected project. GlueKit's observable combinators make it simple to put together data derived from our model objects. The resulting fields in the view model are themselves observable, and react to changes to any of their dependencies on their own.

class ProjectSummaryViewModel {
    let currentDocument: Variable<Document> = ...
    let currentAccount: Variable<Account?> = ...
    
    let project: Variable<Project> = ...
    
    /// The name of the current project.
	var projectName: Updatable<String> { 
	    return project.map { $0.name } 
	}
	
    /// The number of issues (open and closed) in the current project.
	var isssueCount: AnyObservableValue<Int> { 
	    return project.selectCount { $0.issues }
	}
	
    /// The number of open issues in the current project.
	var openIssueCount: AnyObservableValue<Int> { 
	    return project.selectCount({ $0.issues }, filteredBy: { $0.isOpen })
	}
	
    /// The ratio of open issues to all issues, in percentage points.
    var percentageOfOpenIssues: AnyObservableValue<Int> {
        // You can use the standard arithmetic operators to combine observables.
    	return AnyObservableValue.constant(100) * openIssueCount / issueCount
    }
    
    /// The number of open issues assigned to the current account.
    var yourOpenIssues: AnyObservableValue<Int> {
        return project
            .selectCount({ $0.issues }, 
                filteredBy: { $0.isOpen && $0.owner == self.currentAccount })
    }
    
    /// The five most recently created issues assigned to the current account.
    var yourFiveMostRecentIssues: AnyObservableValue<[Issue]> {
        return project
            .selectFirstN(5, { $0.issues }, 
                filteredBy: { $0.isOpen && $0.owner == currentAccount }),
                orderBy: { $0.created < $1.created })
    }

    /// An observable version of NSLocale.currentLocale().
    var currentLocale: AnyObservableValue<NSLocale> {
        let center = NSNotificationCenter.defaultCenter()
		let localeSource = center
		    .source(forName: NSCurrentLocaleDidChangeNotification)
		    .map { _ in NSLocale.currentLocale() }
        return AnyObservableValue(getter: { NSLocale.currentLocale() }, futureValues: localeSource)
    }
    
    /// An observable localized string.
    var localizedIssueCountFormat: AnyObservableValue<String> {
        return currentLocale.map { _ in 
            return NSLocalizedString("%1$d of %2$d issues open (%3$d%%)",
                comment: "Summary of open issues in a project")
        }
    }
    
    /// An observable text for a label.
    var localizedIssueCountString: AnyObservableValue<String> {
        return AnyObservableValue
            // Create an observable of tuples containing values of four observables
            .combine(localizedIssueCountFormat, issueCount, openIssueCount, percentageOfOpenIssues)
            // Then convert each tuple into a single localized string
            .map { format, all, open, percent in 
                return String(format: format, open, all, percent)
            }
    }
}

(Note that some of the operations above aren't implemented yet. Stay tuned!)

Whenever the model is updated or another project or account is selected, the affected Observables in the view model are recalculated accordingly, and their subscribers are notified with the updated values. GlueKit does this in a surprisingly efficient manner---for example, closing an issue in a project will simply decrement a counter inside openIssueCount; it won't recalculate the issue count from scratch. (Obviously, if the user switches to a new project, that change will trigger a recalculation of that project's issue counts from scratch.) Observables aren't actually calculating anything until and unless they have subscribers.

Once you have this view model, the view controller can simply subscribe its observables to various labels displayed in the view hierarchy:

class ProjectSummaryViewController: UIViewController {
    private let visibleConnections = Connector()
    let viewModel: ProjectSummaryViewModel
    
    // ...
    
    override func viewWillAppear() {
        super.viewWillAppear()
        
	    viewModel.projectName.values
	        .subscribe { name in
	            self.titleLabel.text = name
	        }
	        .putInto(visibleConnections)
	     
	    viewModel.localizedIssueCountString.values
	        .subscribe { text in
	            self.subtitleLabel.text = text
	        }
	        .putInto(visibleConnections)
	        
        // etc. for the rest of the observables in the view model
    }
    
    override func viewDidDisappear() {
        super.viewDidDisappear()
        visibleConnections.disconnect()
    }
}

Setting up the connections in viewWillAppear ensures that the view model's complex observer combinations are kept up to date only while the project summary is displayed on screen.

The projectName property in ProjectSummaryViewModel is declared an Updatable, so you can modify its value. Doing that updates the name of the current project:

viewModel.projectName.value = "GlueKit"   // Sets the current project's name via a key path
print(viewModel.project.name.value)       // Prints "GlueKit"
Comments
  • Implement transactional updates

    Implement transactional updates

    Given an observable integer a, we can define the observable sum of adding it to itself:

    import GlueKit
    let a = Variable<Int>(2)
    
    let sum = a + a
    

    The value of sum seems to track the original observable correctly:

    sum.value      // => 4
    a.value = 1
    sum.value      // => 2
    

    However, if we look into the changes reported by sum, we find something surprising: apparently the result of adding an integer to itself can temporarily look like an odd number?!

    var reported: [String] = []
    let connection = sum.changes.connect { change in reported.append("\(change.old)→\(change.new)") }
    a.value = 2
    reported.joined(separator: ", ")  // => "2→3, 3→4"
    connection.disconnect()
    

    The reason for this is quite simple: as a changes from 1 to 2, the plus operator in a + a receives two changes, for both of its dependencies. It doesn't know these changes aren't independent, so it reports two separate changes to itself, going from 2 to 3, then 3 to 4.

    These spurious notifications can cause issues when we build complex observable expressions. At the very least, they mean more changes are generated than strictly necessary, costing performance. But the real problem is that such temporary values aren't valid, and shouldn't be reported at all.

    opened by lorentey 3
  • Installation Section

    Installation Section

    Hey, your library is really interesting.

    The only problem I found was the README.md, which lacks an Installation Section I created this iOS Open source Readme Template so you can take a look on how to easily create an Installation Section If you want, I can help you to organize the lib.

    What are your thoughts? 😄

    opened by lfarah 3
  • Fix two-way bindings

    Fix two-way bindings

    As noted in issue #5, two-way bindings have not yet been updated to support transactional updates.

    https://github.com/lorentey/GlueKit/blob/a0ed7f17fe0c05380270554c4ef79151f4cb473e/Sources/UpdatableValue.swift#L151-L171

    Update the code to take transactions into account. This is more difficult than it seems! E.g., when b is bound to a, a beginTransaction update from a should immediately trigger the same from b. However, b's resulting beginTransaction message must not loop back to a, even though there is a two-way binding between them!

    opened by lorentey 2
  • Add variant of ObservableArrayType.reduce that reduces over a mapping

    Add variant of ObservableArrayType.reduce that reduces over a mapping

    Mapping an array requires maintaining an index mapping, which is superfluous when we're going to ignore the ordering anyway; so array.map{$0.someInteger}.sum() does extra work.

    opened by lorentey 1
  • Observing every element of a collection is too expensive

    Observing every element of a collection is too expensive

    Composite observables often need to observe many dependencies at once; e.g. array.flatMap { $0.observableField } needs to observe not only the array itself, but also the specified observable field of each of its individual elements. Currently, each individual observation requires at least five allocations:

    • The subscription sink itself is always a closure that notifies the dependent observable; it is heap-allocated (allocation no. 1).
    • The Connection disposable that controls the lifetime of the subscription is a heap-allocated object (allocation no. 2).
    • Connection holds an array of disconnection callbacks and a lock:
      • The lock is of a class type (allocation no. 3).
      • The array's storage is heap allocated (allocation no. 4).
      • There is always at least one disconnection callback; it's a closure with a strong reference to the associated signal. It's heap-allocated (allocation no. 5).

    This seems incredibly wasteful, and it should be changed to minimize the number of allocations. Ideally, we'd be able to subscribe to a set of observables with no per-subscription allocations.

    Notes about fixing this

    • SourceType should not be defined in terms of the type-lifted Sink struct; its connect function should be generic, so that we can use sinks that don't contain closures:

      public protocol SourceType {
         associatedtype SourceValue
         func connect<S: SinkType>(_ sink: S) -> Connection where S.SinkValue == SourceValue
      }
      struct FooSink {  // A sink that doesn't need anything allocated
        let target: Foo
        func receive(_ value: Int) { target.doSomething(value) }
      }
      

      (I think most aggregate sinks will fit the space reserved for such generic parameters without the allocation of a box.)

    • We could convert Connection into a class hierarchy, and move the responsibility of storing the sinks into it. I.e., define

      class ConcreteConnection<Source: SourceType, Sink: SinkType>: BaseConnection, SinkType
      where Source.SourceValue == Sink.SinkValue {
        typealias Value = Source.SourceValue
        var source: Source
        var sink: Sink
      
        deinit { disconnect() }
        func disconnect() { source?.disconnect(self); source = nil; sink = nil }
        func receive(_ value: Value) { sink?.receive(value) }
      }
      

      Connection is currently a mish-mash of various hooks, bells and whistles (RefListElement, locking, additional callbacks, etc.). These should be ripped out and/or replaced by specific subclasses, as needed.

      In this approach, sources wouldn't create the connections any more — the connection would be created outside the source, then given to it using new API. (In this scheme, Connection should probably be renamed Sink, replacing the existing sink concept.)

    • Ideally, Connection would be a protocol so that structs could also implement it. However, the protocol must not have associated types, so that it has existentials that can be stored in a collection. Therefore, it cannot contain the receive function above. However, the connection has a reference to the source and knows all types involved, so we could use it to define a workaround:

      public protocol SourceType {
         associatedtype SourceValue
         func register(_ connection: Connection)
         func unregister(_ connection: Connection)
         func _getValue() -> SourceValue // Only callable from Connection.receiveValue()
      }
      protocol Connection {
        func disconnect()
        func receiveValue() // Calls source._getValue() to get the value, and sends it to the sink
      }
      

      (Hopefully there is a way to make this less ugly.) This approach means that a single connection may not subscribe to more than one source at a time -- but that's not really a limitation, since instantiating a new collection gets much cheaper.

    • A binary observable operator like + could then simply observe its dependencies without any allocations:

      struct LeftSink<Source: SourceType, Value: IntegerArithmetic>: SinkType, Connection 
      where Source.SourceValue == SimpleChange<Value> {
        let source: Source
        let target: BinaryCompositeObservable<Value>
        func receive(_ change: SimpleChange<Value>) { target.applyLeft(change) }
      }
      // struct RightSink is defined similarly.
      
      class BinaryCompositeObservable: ... {
        init<O: ObservableType>(left: O, right: O, combinator: @escaping (Value, Value) -> Value) {
           …
           left.changes.register(LeftSink(source: left.changes, target: self))
           right.changes.register(RightSink(source: left.changes, target: self))
        }
        deinit {
            left.unregister(LeftSink(source: left.changes, target: self))
            right.unregister(RightSink(source: left.changes, target: self))
        }
      }
      
    • All of this should be done so that the closure-based API remains unchanged:

      let connection = signal.connect { value in print(value) }
      signal.send(2)
      signal.send(3)
      connection.disconnect()
      

      The compatibility definition of connect above would simply create a Connection/Sink containing the specified closure, register it to the source, and return it.

    opened by lorentey 1
  • Implement cheaper subscriptions

    Implement cheaper subscriptions

    This PR massively reduces the cost of observations inside GlueKit's transformations by eliminating most heap allocations.

    • From now on, types implementing SinkType need to be Hashable, providing an identity for subscribers. This gets rid of the need for a separate mechanism to assign identity to sinks, which simplifies Signal.
    • SourceType.connect, which was previously the primary subscription interface, has been replaced by two methods: add and remove. Both of them take a single sink argument. remove returns the original sink instance; this is important when the sink has some hidden internal state on its own.
    • The original closure-based connect interface is still provided as an extension method. It remains the preferred way to subscribe to observables outside of GlueKit.
    • As a bonus change, UpdatableType has been improved by replacing withTransaction by an apply function that can take any update. This is far more generic and useful.

    Remaining allocations:

    1. Signals (and storage for the Set inside them) are lazily allocated when the first subscriber arrives at an observable. (Inlining Signals would eliminate one more allocation; but this is best done after #5 has been implemented — Signal implementation is much too complicated right now.)
    2. SinkType is a protocol with an associated type, so Swift does not currently provide a protocol existential for it. GlueKit needs to implement type erasure using class polymorphism; so each sink needs to be wrapped inside a heap-allocated class instance. (I assume if generalized protocol existentials get implemented, they will include limited space for inline storage of small values, like Any does. Most GlueKit-internal sinks are of three words or less, which would hopefully fit inside the existential without boxing.)

    This fixes issue #3.

    opened by lorentey 0
  • Creating/destroying two-way bindings is unsupported during transactions

    Creating/destroying two-way bindings is unsupported during transactions

    Issue #6 updated bindings for the new transactional updates, but it did not add full support. Specifically, setting up or tearing down two-way bindings during an active transaction remains unsupported, and traps if you try.

    It is unclear if this will cause problems in practical apps — assuming bindings are set up during in UIViewController's viewWillAppear and destroyed in viewDidDisappear, they would normally be outside of any active transaction.

    If it did cause problems, however, we have are a number of ways available to support mid-transaction bindings.

    1. Delay formally establishing/destroying the binding until the current transaction finishes. This seems easy enough to do (we just need to add a little more complicated state machine in BindConnection/BindSink), but it might break people's assumptions about how bindings interact with transactions. Note that changes must start getting propagated immediately when the binding is set up, and the value must stop getting synced immediately after unbinding—only transaction boundaries may get fuzzed when necessary. Bindings may survive BindConnection's deinit in this system, so state management must be moved to a new private class.

    2. Handle the underlying issue of needing to keep track of the origin updatable for each transaction. (When unbinding the two updatables, we need to split the pool of active transactions by assigning each to the updatable that originated it.) One way to do this would be to change the transaction system such that not just the outermost begin/end messages are propagated to subscribers, but also nested ones. We would probably also need some form of unique transaction ID embedded in begin/end messages — the binding needs to recognize loopbacks for messages it forwarded itself and differentiate them from messages notifying about nested transactions coming from outside.

    3. We could perhaps collapse updatables that are bound together into a single logical entity. The original components would still keep track of their subscribers and number of outstanding transactions, but they would let the unified entity handle all messaging. The problem with this approach is that it would considerably complicate the Source API.

    opened by lorentey 0
  • Retrocausality is possible

    Retrocausality is possible

    Issue #5 disallows updates to an Observable from inside its own observers, but observers may still synchronously modify the state of other parts of the system.

    This means that an observer to variable a is free to directly update the value of variable b, causing b's observers to be notified of the change immediately, even though a may not have finished notifying all of its observers of the original change. If an object was subscribed to both variables, it may thus receive b's notification before it sees the change to a that caused it — it may observe the effect before the cause!

    Such retrocausality is against FRP's principles, but it may not actually cause serious problems. Find out if it needs to be fixed.

    opened by lorentey 3
  • Disallow reentrant updates

    Disallow reentrant updates

    As an experiment, GlueKit's signal implementation currently provides support for reentrant sends; e.g., you are free to change the value of a variable from one of the subscribers to it:

    let foo = Variable<Int>(4)
    let c = foo.values.connect { v in 
        // If v is even, set it to a nearby odd value.
        if v & 1 == 0 { 
            foo.value = v + 1
        } 
    }
    print(foo.value)      // => 5
    foo.value = 6
    print(foo.value)      // => 7
    c.disconnect()
    

    Values that are sent while a signal is already sending values are queued up and sent asynchronously. Implementing this has been fun, but I wonder if it is worth the additional complexity.

    Background

    For reference, no reactive framework I know supports reentrant sends; they all deadlock on the first nested send invocation. However, KVO does support updating a property from one of its observers. When the property's value changes during one of KVO's signaling loops,

    1. KVO performs a new, nested, synchronous signaling loop notifying all observers of the change immediately. The observer that was responsible for the nested change receives a nested observeValue(forKeyPath:,…) call.
    2. Once the nested loop finishes, it completes the original signaling loop. However, it doesn't send the original notification payload, because that describes an outdated change — it updates the notification to match the latest value of the property.

    KVO can do this because its signaling subsystem is written specifically for the purpose of sending change notifications, so it can update the notification payload as it is being sent, by merging interim changes into the original update.

    GlueKit's Signal is a general signaling mechanism where in-flight values cannot be modified. So the only reasonable way to implement nested sends is to send their values asynchronously, after the currently running signaling loop completes. This means that the values that are received by observers are different in the two systems:

    https://github.com/lorentey/GlueKit/blob/a0ed7f17fe0c05380270554c4ef79151f4cb473e/Tests/KVOSupportTests.swift#L217-L284

    GlueKit's Signal is more consistent in the sense that everyone receives the same values, in the order they were sent. But KVO's implementation makes a little more sense in the context of change notifications — the value that observers see always matches what's in their notification payload.

    Why is it a pain to support nested sends

    The possibility that sending a value to an observer may trigger a nested send immensely complicates matters in a variety of common scenarios. For instance, sending a "welcome" value to new observers, like ObservableValueType.values does, becomes horribly complicated:

    https://github.com/lorentey/GlueKit/blob/a0ed7f17fe0c05380270554c4ef79151f4cb473e/Sources/ObservableValue.swift#L62-L82

    (The code is so complicated because it needs to handle the case where sink.receive(value) changes the value; for consistency, we want the sink to receive updates about its own changes, too. Also, connecting the sink to the source may send values to it by side effect (like values does), and those values need to be ordered consistently, too.)

    I expect such nested sends will occur extremely rarely (if at all) in normal use-cases, but writing code to support them is unreasonably hard, and comes at a (probably) measurable cost of performance at runtime.

    Furthermore, it is very easy to forget nested sends might occur; I'm sure the code already has instances where a nested send may lead to an inconsistent sequence of notifications.

    How to remove support for reentrant sends

    Signal can be easily simplified to remove support for nested sends; that's not a problem. Ideally a nested send would cause a trap, but deadlocking is fine, too, and is probably much easier to implement.

    Other code that can be simplified can be found by a review of the codebase. (It's OK if we miss a couple.)

    We need to consider whether KVO's support for reentrancy causes problems for the adapter code in KVO Support.swift. At first glance it doesn't, because recursive observeValue(forKeyPath:…) calls only happen for the actual observers that cause nested updates; other observers only get called strictly sequentially. (The fact that other reactive frameworks had no real issues with their own KVO adapters indicate this is correct.)

    What about two-way bindings?

    GlueKit includes rudimentary support for binding two updatables of the same Equatable type together.

    https://github.com/lorentey/GlueKit/blob/a0ed7f17fe0c05380270554c4ef79151f4cb473e/Sources/UpdatableValue.swift#L151-L171

    This feature has seen very limited use in production, and it is as yet unclear if the concept is robust enough. It has not even been updated yet for transactional changes. But supposing it is updated, removing support for nested sends does not cause problems at first glance — the equality check is there to prevent an infinite update loop, and it also does a good job of preventing even a single reentrant update. (Corner cases need to be dealt with, though. In the absence of nested updates, I don't think a Behavior's current value can ever differ from the one described in one its update Events, but I may be wrong.)

    opened by lorentey 1
Releases(v0.2.0)
Owner
A collection of useful Swift packages
null
A static source code analysis tool to improve quality and reduce defects for C, C++ and Objective-C

OCLint - https://oclint.org OCLint is a static code analysis tool for improving quality and reducing defects by inspecting C, C++ and Objective-C code

The OCLint Static Code Analysis Tool 3.6k Dec 29, 2022
A tool for Swift code modification intermediating between code generation and formatting.

swift-mod A tool for Swift code modification intermediating between code generation and formatting. Overview swift-mod is a tool for Swift code modifi

Ryo Aoyama 95 Nov 3, 2022
SwiftCop is a validation library fully written in Swift and inspired by the clarity of Ruby On Rails Active Record validations.

SwiftCop is a validation library fully written in Swift and inspired by the clarity of Ruby On Rails Active Record validations. Objective Build a stan

Andres Canal 542 Sep 17, 2022
A command-line tool and Xcode Extension for formatting Swift code

Table of Contents What? Why? How? Command-line tool Xcode source editor extension Xcode build phase Via Applescript VSCode plugin Sublime Text plugin

Nick Lockwood 6.3k Jan 8, 2023
A tool to enforce Swift style and conventions.

SwiftLint A tool to enforce Swift style and conventions, loosely based on the now archived GitHub Swift Style Guide. SwiftLint enforces the style guid

Realm 16.9k Jan 9, 2023
Cross-platform static analyzer and linter for Swift.

Wiki • Installation • Usage • Features • Developers • License Tailor is a cross-platform static analysis and lint tool for source code written in Appl

Sleekbyte 1.4k Dec 19, 2022
Trackable is a simple analytics integration helper library. It’s especially designed for easy and comfortable integration with existing projects.

Trackable Trackable is a simple analytics integration helper library. It’s especially designed for easy and comfortable integration with existing proj

Vojta Stavik 145 Apr 14, 2022
Makes it easier to support older versions of iOS by fixing things and adding missing methods

PSTModernizer PSTModernizer carefully applies patches to UIKit and related Apple frameworks to fix known radars with the least impact. The current set

PSPDFKit Labs 217 Aug 9, 2022
Find common xib and storyboard-related problems without running your app or writing unit tests.

IBAnalyzer Find common xib and storyboard-related problems without running your app or writing unit tests. Usage Pass a path to your project to ibanal

Arek Holko 955 Oct 15, 2022
Skredvarsel app - an iOS, iPadOS, and macOS application that provides daily avalanche warnings from the Norwegian Avalanche Warning Service API

Skredvarsel (Avalanche warning) app is an iOS, iPadOS, and macOS application that provides daily avalanche warnings from the Norwegian Avalanche Warning Service API

Jonas Follesø 8 Dec 15, 2022
Simple iOS app blackbox assessment tool. Powered by frida.re and vuejs.

Discontinued Project This project has been discontinued. Please use the new Grapefruit #74 frida@14 compatibility issues frida@14 introduces lots of b

Chaitin Tech 1.6k Dec 16, 2022
Exclude files and folders from Alfred’s search results

Ignore in Alfred Alfred Workflow Exclude files and folders from Alfred’s search results ⤓ Download Workflow About The macOS metadata search API only a

Alfred 10 Dec 13, 2022
Lint anything by combining the power of Swift & regular expressions.

Installation • Getting Started • Configuration • Xcode Build Script • Donation • Issues • Regex Cheat Sheet • License AnyLint Lint any project in any

Flinesoft 116 Sep 24, 2022
An Xcode formatter plug-in to format your swift code.

Swimat Swimat is an Xcode plug-in to format your Swift code. Preview Installation There are three way to install. Install via homebrew-cask # Homebrew

Jintin 1.6k Jan 7, 2023
💊 Syntactic sugar for Swift do-try-catch

Fallback Syntactic sugar for Swift do-try-catch. At a Glance value = try fallback( try get("A"), try get("B"), try get("C"), try get("D") ) is

Suyeol Jeon 43 May 25, 2020
A Swift micro-framework to easily deal with weak references to self inside closures

WeakableSelf Context Closures are one of Swift must-have features, and Swift developers are aware of how tricky they can be when they capture the refe

Vincent Pradeilles 72 Sep 1, 2022
Swift-lint-plugin - A SwiftPM plugin that adds a linting command

SwiftLintPlugin This is a SwiftPM plugin that adds a lint command. SwiftPM plugi

null 9 Nov 23, 2022
Aplicação Basica em Swift desenvolvida com o intuito de aplicar os conceitos estudados

Via Cep iOS Sobre - Interface do Usuario - Tecnologias - Requisitos - Autor Projeto ?? FINALIZADO ?? Sobre A Aplicação consiste em fazer buscas usando

Igor Damasceno de Sousa 1 Jun 3, 2022
Type-safe CAAnimation wrapper. It makes preventing to set wrong type values.

TheAnimation TheAnimation is Type-safe CAAnimation wrapper. Introduction For example, if you want to animate backgroundColor with CABasicAnimation, yo

Taiki Suzuki 222 Dec 6, 2022
Observable is the easiest way to observe values in Swift.

Observable is the easiest way to observe values in Swift. How to Create an Observable and MutableObservable Using MutableObservable you can create and

Robert-Hein Hooijmans 368 Nov 9, 2022