Skip to content

Conversation

@andersio
Copy link
Member

@andersio andersio commented May 22, 2017

Related: #309.

There are legitimate uses of isExecuting feedback to the Action state property. For example, we may link the availability of multiple actions together, so that all disables when one executes.

This PR refactors the internal of Action so that such use cases are permitted. Legitimate infinite feedback loops would still result in deadlocks.

Example:

let isIdle = MutableProperty(true)

candyCrusher = Action(enabledIf: isIdle) { ... }
fruitCrusher = Action(enabledIf: isIdle) { ... }
carrotCrusher = Action(enabledIf: isIdle) { ... }

isIdle <~ candyCrusher.isExecuting.negate()
isIdle <~ fruitCrusher.isExecuting.negate()
isIdle <~ carrotCrusher.isExecuting.negate()

Alternative/Follow-up

Could we have some kind of "action group"s that determine the availability at a coarser granularity?

@andersio andersio added the bug label May 22, 2017
@andersio andersio force-pushed the action-recursive-enabled branch 3 times, most recently from d110566 to abc9160 Compare May 22, 2017 07:55
@andersio andersio modified the milestone: 2.0 May 22, 2017

state.availability = .enabledExecuting
isExecuting.value = true
isEnabled.value = false
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is isEnabled false if availability is enabledExecuting?

Copy link
Member Author

Choose a reason for hiding this comment

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

Action s are disabled when executing.

let isExecuting = MutableProperty(false)
let isEnabled = MutableProperty(true)
self.isExecuting = Property(capturing: isExecuting)
self.isEnabled = Property(capturing: isEnabled)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can these be derived from actionState instead of duplicating that information in separate properties?

Copy link
Member Author

@andersio andersio May 24, 2017

Choose a reason for hiding this comment

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

The signal of ActionState is deliberately ignored, since Signal doesn't support recursion with value events.

actionState deadlocks when one does isEnabled <~ isExecuting and both are derived from it, i.e. the current implementation.

Copy link
Member Author

@andersio andersio May 26, 2017

Choose a reason for hiding this comment

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

In 4acaf81, the read-only isEnabled is once again derived since we are permitting only state <~ isExecuting here, where state is the state of the Action that flips its availability (isEnabled).

state.availability = .disabledExecuting

default:
break
Copy link
Contributor

Choose a reason for hiding this comment

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

This default shouldn't be needed?

Copy link
Member Author

@andersio andersio May 24, 2017

Choose a reason for hiding this comment

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

There are eight combinations in total, and only four need to be acted on.

executeClosure = { state, input in execute(state as! State.Value, input) }
// `isExecuting` and `isEnabled` should use their own locks, so that legitimate
// feedbacks would not deadlock.
let initial = (availability: Availability.enabledIdle, value: state.value)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you make this a struct instead of a typealias?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done in 4acaf81.

case enabledIdle
case enabledExecuting
case disabledIdle
case disabledExecuting
Copy link
Contributor

Choose a reason for hiding this comment

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

This is really modeling two independent boolean states. Can we separate these and add them to ActionState individually? I think that will simplify the code a bit as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

The problem is that isEnabled and isExecuting are not independent. They together form a finite state machine, and IMO named cases are easier to understand than a switch with tuples of booleans.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed in 4acaf81 anyway.

/// In other words, this sends every value from every unit of work that the `Action`
/// executes.
public let values: Signal<Output, NoError>
public private(set) lazy var values: Signal<Output, NoError> = { [unowned self] in
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we make this lazy change in a separate PR? I'm not sold on it, and it's not essential to this PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

Reverted in 4acaf81.

@tikitu tikitu mentioned this pull request May 25, 2017
@andersio andersio force-pushed the action-recursive-enabled branch 5 times, most recently from abbf01d to 4b6792e Compare May 26, 2017 20:00
@andersio andersio force-pushed the action-recursive-enabled branch from 4b6792e to 4acaf81 Compare May 26, 2017 20:02
@andersio andersio requested a review from mdiep May 26, 2017 20:05
self.isEnabled = actionState.map { $0.isEnabled }

let isExecuting = MutableProperty(false)
self.isExecuting = Property(capturing: isExecuting)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be self.isExecuting = actionState.map { $0.isExecuting }?

Copy link
Member Author

@andersio andersio May 31, 2017

Choose a reason for hiding this comment

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

The point of giving it its own backing is that isExecuting calls out to the observers outside the signal of actionState, so that when actionState is modified reactively & synchronously it doesn't deadlock.

However, it does seem violating exclusivity of access so it might need a change anyway. We might need to add guards in MutableProperty to prevent nested modifications too, or rely on the dynamic endorcement in Swift 4.

self.isExecuting = Property(capturing: isExecuting)

// Associate the state property with the created `Action`.
lifetime.observeEnded { _ = state }
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment is describing what this line does. Can you update it to describe why it needs to be associated?

state = MutableProperty(initial)
self.execute = { action, input in
return SignalProducer { observer, lifetime in
func tryStart() -> State.Value? {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is only called once. Maybe take the code out of the function?

let state = actionState.modify { state in
    ...
}

guard let state = state else {
...

It should be done after the modifier of `ActionState` returns and the `inout` reference is semantically written back, in order not to violate the exclusivity of access.

self.isEnabled = state.map { $0.isEnabled }.skipRepeats()
self.isExecuting = state.map { $0.isExecuting }.skipRepeats()
let latestState: State.Value? = actionState.modify(didSet: didSet) { state in
Copy link
Contributor

Choose a reason for hiding this comment

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

This didSet seem pointless. The function is only called here, right after notifiesExecutionState is set to false—so it will always do nothing. It seem like both didSet() and notifiesExecutionState can be removed?

Copy link
Member Author

Choose a reason for hiding this comment

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

notifiesExecutionState is set to true when the current execution attempt succeeds and starts. didSet is invoked after the trailing closure modifying actionState.

Copy link
Contributor

Choose a reason for hiding this comment

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

🤦🏼‍♂️


self.isEnabled = state.map { $0.isEnabled }.skipRepeats()
self.isExecuting = state.map { $0.isExecuting }.skipRepeats()
let latestState: State.Value? = actionState.modify(didSet: didSet) { state in
Copy link
Contributor

Choose a reason for hiding this comment

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

🤦🏼‍♂️

@mdiep mdiep merged commit 45c2bd4 into master May 31, 2017
@mdiep mdiep deleted the action-recursive-enabled branch May 31, 2017 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants