Skip to content

Commit

Permalink
Merge branch 'master' into producer-lift
Browse files Browse the repository at this point in the history
  • Loading branch information
andersio committed Jan 12, 2017
2 parents d2cc8fc + b26b6c3 commit 1294573
Show file tree
Hide file tree
Showing 17 changed files with 444 additions and 60 deletions.
54 changes: 47 additions & 7 deletions Documentation/DesignGuidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ resource for getting up to speed on the main types and concepts provided by Reac
1. [`completion` indicates success](#completion-indicates-success)
1. [`interruption`s cancel outstanding work and usually propagate immediately](#interruptions-cancel-outstanding-work-and-usually-propagate-immediately)
1. [Events are serial](#events-are-serial)
1. [Events cannot be sent recursively](#events-cannot-be-sent-recursively)
1. [Events are never delivered recursively, and values cannot be sent recursively](#events-are-never-delivered-recursively-and-values-cannot-be-sent-recursively)
1. [Events are sent synchronously by default](#events-are-sent-synchronously-by-default)

**[The `Signal` contract](#the-signal-contract)**
Expand All @@ -33,6 +33,14 @@ resource for getting up to speed on the main types and concepts provided by Reac
1. [Signal operators can be lifted to apply to signal producers](#signal-operators-can-be-lifted-to-apply-to-signal-producers)
1. [Disposing of a produced signal will interrupt it](#disposing-of-a-produced-signal-will-interrupt-it)


**[The Property contract](#the-property-contract)**

1. [A property must have its latest value sent synchronously accessible](#a-property-must-have-its-latest-value-sent-synchronously-accessible)
1. [Events must be synchronously emitted after the mutation is visible](#events-must-be-synchronously-emitted-after-the-mutation-is-visible)
1. [Reentrancy must be supported for reads](#reentrancy-must-be-supported-for-reads)
1. [A composed property does not have a side effect on its sources, and does not own its lifetime](#a-composed-property-does-not-have-a-side-effect-on-its-sources-and-does-not-own-its-lifetime)

**[Best practices](#best-practices)**

1. [Process only as many values as needed](#process-only-as-many-values-as-needed)
Expand Down Expand Up @@ -143,18 +151,20 @@ simultaneously.

This simplifies [operator][Operators] implementations and [observers][].

#### Events cannot be sent recursively
#### Events are never delivered recursively, and values cannot be sent recursively.

Just like ReactiveSwift guarantees that [events will not be received
concurrently](#events-are-serial), it also guarantees that they won’t be
received recursively. As a consequence, [operators][] and [observers][] _do not_ need to
Just like [the guarantee of events not being delivered
concurrently](#events-are-serial), it is also guaranteed that events would not be
delivered recursively. As a consequence, [operators][] and [observers][] _do not_ need to
be reentrant.

If an event is sent upon a signal from a thread that is _already processing_
a previous event from that signal, deadlock will result. This is because
If a `value` event is sent upon a signal from a thread that is _already processing_
a previous event from that signal, it would result in a deadlock. This is because
recursive signals are usually programmer error, and the determinacy of
a deadlock is preferable to nondeterministic race conditions.

Note that a terminal event is permitted to be sent recursively.

When a recursive signal is explicitly desired, the recursive event should be
time-shifted, with an operator like [`delay`][delay], to ensure that it isn’t sent from
an already-running event handler.
Expand Down Expand Up @@ -304,6 +314,36 @@ everything added to the [`CompositeDisposable`][CompositeDisposable] in
Note that disposing of one produced `Signal` will not affect other signals created
by the same `SignalProducer`.

## The Property contract.

A property is essentially a `Signal` which guarantees it has an initial value, and its latest value is always available for being read out.

All read-only property types should conform to `PropertyProtocol`, while the mutable counterparts should conform to `MutablePropertyProtocol`. ReactiveSwift includes two primitives that implement the contract: `Property` and `MutableProperty`.

#### A property must have its latest value sent synchronously accessible.

A property must have its latest value cached or stored at any point of time. It must be synchronously accessible through `PropertyProtocol.value`.

The `SignalProducer` of a property must replay the latest value before forwarding subsequent changes, and it may ensure that no race condition exists between the replaying and the setup of the forwarding.

#### Events must be synchronously emitted after the mutation is visible.

A mutable property must emit its values and the `completed` event synchronously.

The observers of a property should always observe the same value from the signal and the producer as `PropertyProtocol.value`. This implies that all observations are a `didSet` observer.

#### Reentrancy must be supported for reads.

All properties must guarantee that observers reading `PropertyProtocol.value` would not deadlock.

In other words, if a mutable property type implements its own, or inherits a synchronization mechanism from its container, the synchronization generally should be reentrant due to the requirements of synchrony.

#### A composed property does not have a side effect on its sources, and does not own its lifetime.

A composed property presents a transformed view of its sources. It should not have a side effect on them, as [observing a signal does not have side effects](#observing-a-signal-does-not-have-side-effects) either. This implies a composed property should never retain its sources, or otherwise the `completed` event emitted upon deinitialization would be influenced.

Moreover, it does not own its lifetime, and its deinitialization should not affect its signal and its producer. The signal and the producer should respect the lifetime of the ultimate sources in a property composition graph.

## Best practices

The following recommendations are intended to help keep ReactiveSwift-based code
Expand Down
48 changes: 21 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,17 @@ currentTime.observeValues { timeBar.timeLabel.text = "\($0)" }
#### `Action`: a serialized worker with a preset action.
When being invoked with an input, `Action` apply the input and the latest state to the preset action, and pushes the output to any interested parties.

It is like an automatic vending machine — after choosing an option with coins inserted, the machine would process the order and eventually output your wanted snacks. Notice that the entire process is mutually exclusive — you cannot have the machine to serve two customers concurrently.
It is like an automatic vending machine — after choosing an option with coins inserted, the machine would process the order and eventually output your wanted snack. Notice that the entire process is mutually exclusive — you cannot have the machine to serve two customers concurrently.

```swift
// Purchase from the vending machine with a specific option.
vendingMachine.purchase
.apply(snackId)
.startWithResult { result
switch result {
case let .success(snacks):
print("Snack: \(snacks)")
case let .success(snack):
print("Snack: \(snack)")

case let .failure(error):
// Out of stock? Insufficient fund?
print("Transaction aborted: \(error)")
Expand All @@ -82,18 +82,19 @@ vendingMachine.purchase

// The vending machine.
class VendingMachine {
let purchase: Action<(), [Snack], VendingMachineError>
let purchase: Action<Int, Snack, VendingMachineError>
let coins: MutableProperty<Int>

// The vending machine is connected with a sales recorder.
init(_ salesRecorder: SalesRecorder) {
coins = MutableProperty(0)
purchase = Action(state: coins, enabledIf: { $0 > 0 }) { coins, snackId in
purchase = Action(state: coins, enabledIf: { $0 > 0 }) { coins, snackId in
return SignalProducer { observer, _ in
// The sales magic happens here.
// Fetch a snack based on its id
}
}

// The sales recorders are notified for any successful sales.
purchase.values.observeValues(salesRecorder.record)
}
Expand Down Expand Up @@ -163,7 +164,7 @@ one. Just think of how much code that would take to do by hand!

#### Receiving the results

Since the source of search strings is a `Signal` which has a hot signal semantic,
Since the source of search strings is a `Signal` which has a hot signal semantic,
the transformations we applied are automatically evaluated whenever new values are
emitted from `searchStrings`.

Expand All @@ -174,10 +175,10 @@ searchResults.observe { event in
switch event {
case let .value(results):
print("Search results: \(results)")

case let .failed(error):
print("Search error: \(error)")

case .completed, .interrupted:
break
}
Expand Down Expand Up @@ -279,13 +280,13 @@ let searchString = textField.reactive.continuousTextValues

For more information and advance usage, check the [Debugging Techniques](Documentation/DebuggingTechniques.md) document.

## How does ReactiveSwift relate to Rx?

While ReactiveCocoa was inspired and heavily influenced by [ReactiveX][] (Rx), ReactiveSwift is
an opinionated implementation of [functional reactive programming][], and _intentionally_ not a
## How does ReactiveSwift relate to RxSwift?
RxSwift is a Swift implementation of the [ReactiveX][] (Rx) APIs. While ReactiveCocoa
was inspired and heavily influenced by Rx, ReactiveSwift is an opinionated
implementation of [functional reactive programming][], and _intentionally_ not a
direct port like [RxSwift][].

ReactiveSwift differs from ReactiveX in places that:
ReactiveSwift differs from RxSwift/ReactiveX where doing so:

* Results in a simpler API
* Addresses common sources of confusion
Expand Down Expand Up @@ -369,7 +370,7 @@ If you use [Carthage][] to manage your dependencies, simply add
ReactiveSwift to your `Cartfile`:

```
github "ReactiveCocoa/ReactiveSwift" "1.0.0-rc.3"
github "ReactiveCocoa/ReactiveSwift" ~> 1.0
```

If you use Carthage to build your dependencies, make sure you have added `ReactiveSwift.framework`, and `Result.framework` to the "_Linked Frameworks and Libraries_" section of your target, and have included them in your Carthage framework copying build phase.
Expand All @@ -380,7 +381,7 @@ If you use [CocoaPods][] to manage your dependencies, simply add
ReactiveSwift to your `Podfile`:

```
pod 'ReactiveSwift', '1.0.0-rc.3'
pod 'ReactiveSwift', '~> 1.0.0'
```

#### Swift Package Manager
Expand All @@ -389,7 +390,7 @@ If you use Swift Package Manager, simply add ReactiveSwift as a dependency
of your package in `Package.swift`:

```
.Package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", "1.0.0-rc.3")
.Package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", majorVersion: 1)
```

#### Git submodule
Expand Down Expand Up @@ -419,20 +420,13 @@ We also provide a great Playground, so you can get used to ReactiveCocoa's opera
1. Build `ReactiveSwift-macOS` scheme
1. Finally open the `ReactiveSwift.playground`
1. Choose `View > Show Debug Area`

## Have a question?
If you need any help, please visit our [GitHub issues][] or [Stack Overflow][]. Feel free to file an issue if you do not manage to find any solution from the archives.

## Release Roadmap
**Current Stable Release:**<br />[![GitHub release](https://img.shields.io/github/release/ReactiveCocoa/ReactiveSwift.svg)](https://github.com/ReactiveCocoa/ReactiveSwift/releases)

### Code Complete: ReactiveSwift 1.0
It targets Swift 3.0.x. The tentative schedule of a Gold Master release is January 2017.

[Release Candidiate 3](https://github.com/ReactiveCocoa/ReactiveSwift/releases/tag/1.0.0-rc.3) has been released.

A point release is expected with performance optimizations.

### Plan of Record
#### ReactiveSwift 2.0
It targets Swift 3.1.x. The estimated schedule is Spring 2017.
Expand Down
4 changes: 2 additions & 2 deletions ReactiveSwift.podspec
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
Pod::Spec.new do |s|
s.name = "ReactiveSwift"
# Version goes here and will be used to access the git tag later on, once we have a first release.
s.version = "1.0.0-rc.3"
s.version = "1.0.0"
s.summary = "Streams of values over time"
s.description = <<-DESC
ReactiveSwift is a Swift framework inspired by Functional Reactive Programming. It provides APIs for composing and transforming streams of values over time.
DESC
s.homepage = "https://github.com/ReactiveCocoa/ReactiveSwift"
s.license = { :type => "MIT", :file => "LICENSE.md" }
s.author = "ReactiveCocoa"

s.ios.deployment_target = "8.0"
s.osx.deployment_target = "10.9"
s.watchos.deployment_target = "2.0"
Expand Down
12 changes: 6 additions & 6 deletions Sources/Atomic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ import MachO
internal protocol AtomicStateProtocol {
associatedtype State: RawRepresentable

/// Try to transit from the expected current state to the specified next
/// Try to transition from the expected current state to the specified next
/// state.
///
/// - parameters:
/// - expected: The expected state.
///
/// - returns:
/// `true` if the transition succeeds. `false` otherwise.
func tryTransiting(from expected: State, to next: State) -> Bool
func tryTransition(from expected: State, to next: State) -> Bool
}

/// A simple, generic lock-free finite state machine.
Expand Down Expand Up @@ -65,7 +65,7 @@ internal struct UnsafeAtomicState<State: RawRepresentable>: AtomicStateProtocol
value)
}

/// Try to transit from the expected current state to the specified next
/// Try to transition from the expected current state to the specified next
/// state.
///
/// - parameters:
Expand All @@ -74,7 +74,7 @@ internal struct UnsafeAtomicState<State: RawRepresentable>: AtomicStateProtocol
/// - returns:
/// `true` if the transition succeeds. `false` otherwise.
@inline(__always)
internal func tryTransiting(from expected: State, to next: State) -> Bool {
internal func tryTransition(from expected: State, to next: State) -> Bool {
return OSAtomicCompareAndSwap32Barrier(expected.rawValue,
next.rawValue,
value)
Expand Down Expand Up @@ -105,15 +105,15 @@ internal struct UnsafeAtomicState<State: RawRepresentable>: AtomicStateProtocol
return value.modify { $0 == expected.rawValue }
}

/// Try to transit from the expected current state to the specified next
/// Try to transition from the expected current state to the specified next
/// state.
///
/// - parameters:
/// - expected: The expected state.
///
/// - returns:
/// `true` if the transition succeeds. `false` otherwise.
internal func tryTransiting(from expected: State, to next: State) -> Bool {
internal func tryTransition(from expected: State, to next: State) -> Bool {
return value.modify { value in
if value == expected.rawValue {
value = next.rawValue
Expand Down
14 changes: 14 additions & 0 deletions Sources/Deprecations+Removals.swift
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,13 @@ extension SignalProtocol where Error == NoError {
public func observeNext(_ next: (Value) -> Void) -> Disposable? { fatalError() }
}

extension SignalProtocol where Value: Sequence {
@available(*, deprecated, message: "Use flatten() instead")
public func flatten(_ strategy: FlattenStrategy) -> Signal<Value.Iterator.Element, Error> {
return self.flatMap(strategy, transform: SignalProducer.init)
}
}

extension SignalProducerProtocol {
@available(*, unavailable, renamed:"take(first:)")
public func take(_ count: Int) -> SignalProducer<Value, Error> { fatalError() }
Expand Down Expand Up @@ -307,6 +314,13 @@ extension SignalProducerProtocol where Error == NoError {
public func startWithNext(_ value: @escaping (Value) -> Void) -> Disposable { fatalError() }
}

extension SignalProducerProtocol where Value: Sequence {
@available(*, deprecated, message: "Use flatten() instead")
public func flatten(_ strategy: FlattenStrategy) -> SignalProducer<Value.Iterator.Element, Error> {
return self.flatMap(strategy, transform: SignalProducer.init)
}
}

extension SignalProducer {
@available(*, unavailable, message:"Use properties instead. `buffer(_:)` is removed in RAC 5.0.")
public static func buffer(_ capacity: Int) -> (SignalProducer, Signal<Value, Error>.Observer) { fatalError() }
Expand Down
4 changes: 2 additions & 2 deletions Sources/Disposable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ private enum DisposableState: Int32 {
}

extension AtomicStateProtocol where State == DisposableState {
/// Try to transit from `active` to `disposed`.
/// Try to transition from `active` to `disposed`.
///
/// - returns:
/// `true` if the transition succeeds. `false` otherwise.
@inline(__always)
fileprivate func tryDispose() -> Bool {
return tryTransiting(from: .active, to: .disposed)
return tryTransition(from: .active, to: .disposed)
}
}

Expand Down
6 changes: 6 additions & 0 deletions Sources/EventLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ private func defaultEventLog(identifier: String, event: String, fileName: String
}

/// A type that represents an event logging function.
/// Signature is:
/// - identifier
/// - event
/// - fileName
/// - functionName
/// - lineNumber
public typealias EventLogger = (
_ identifier: String,
_ event: String,
Expand Down
18 changes: 8 additions & 10 deletions Sources/Flatten.swift
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,10 @@ extension SignalProtocol where Value: SignalProtocol, Value.Error == NoError {
}
}

extension SignalProtocol where Value: Sequence, Error == NoError {
/// Flattens the `sequence` value sent by `signal` according to
/// the semantics of the given strategy.
public func flatten(_ strategy: FlattenStrategy) -> Signal<Value.Iterator.Element, Error> {
return self.flatMap(strategy) { .init($0) }
extension SignalProtocol where Value: Sequence {
/// Flattens the `sequence` value sent by `signal`.
public func flatten() -> Signal<Value.Iterator.Element, Error> {
return self.flatMap(.merge, transform: SignalProducer.init)
}
}

Expand Down Expand Up @@ -312,11 +311,10 @@ extension SignalProducerProtocol where Value: SignalProtocol, Value.Error == NoE
}
}

extension SignalProducerProtocol where Value: Sequence, Error == NoError {
/// Flattens the `sequence` value sent by `producer` according to
/// the semantics of the given strategy.
public func flatten(_ strategy: FlattenStrategy) -> SignalProducer<Value.Iterator.Element, Error> {
return self.flatMap(strategy) { .init($0) }
extension SignalProducerProtocol where Value: Sequence {
/// Flattens the `sequence` value sent by `signal`.
public func flatten() -> SignalProducer<Value.Iterator.Element, Error> {
return self.flatMap(.merge, transform: SignalProducer.init)
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>1.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
Expand Down
Loading

0 comments on commit 1294573

Please sign in to comment.