Memoization of Swift properties

IIRC, lazy initialisation is also thread-safe. Will this also reset the token for concurrent reads?

:man_shrugging:

1 Like

My mistake, it isn’t thread-safe.

I wonder if we could maybe remodel the existing lazy support as a built-in property wrapper. That would give you a cleaner way to access the underlying storage.

It would need to use magic, but maybe that’s okay.

2 Likes
  • Providing a list of properties used for memoizing may lead to inconsistency. Consider the following case:

I somewhat agree. I think you might be right that by default (if programmer omits a dependency list), Swift can just assume it should recalculate based on any property or variable captured. This is probably the most error-proof default.

However, I think there are still cases where the developer may want the ability to maintain their own list for two reasons:

  1. As pointed out here, there may be times your closure uses a property that doesn't affect the return value. If so, you may want to omit this value to prevent it triggering a recalculation.
  2. There may be times where you want to omit some properties that do affect the return value, if you can still guarantee your logic covers this omission.

For example,

struct SquareBox {
    private(set) var height: Double
    private(set) var width: Double

    private var updateFlag: Bool = false

    mutating func setSides(to length: Double) {
        self.height = length
        self.width = length
        updateFlag = !updateFlag
    }
    
    memo var area(): Double { | updateFlag | in height * width }
}

Ignore the fact that this might not be the best way to set up a square... just pay attention to the fact that height and width (the dependencies) are guaranteed to change together. I'm only letting you change side length via a method that sets both. In this (non-lazy version) if I let Swift manage my properties it would recalculate twice during my setSides call. So instead, I'm creating a private Bool to flip once I know both sides have updated. Then area recalculates only once at the end.

Again, this may be a dumb implementation of a square, but it highlights that cases like these will pop up when you might have to manage your dependency list for optimal efficiency.

However, in a majority of cases, I think we can assume that Swift could auto-generate the dependency list based on whatever was captured in the closure.

You mean something like this?

2 Likes

Yup.. it would be nice if memo was just the default behavior. But there may be a few drawbacks to this (at least in the short term):

  1. My solution doesn't really address reference types (yet... someone may have a good idea... if so, please chime in!). So until someone has a way to guarantee the memoized value will always update (even when the dependency is a reference), it may be detrimental to existing code to apply to everything. At the moment, it's a good solution to opt into some of the time, but it isn't really effective for all situations for now (even though that would be nice).
  2. Sometimes you do need a bit more manual control.

I agree to this. The use of a keyword in my proposal is mostly to stay aligned with lazy, but in many ways an attribute may be preferable.

Yes and no. There are two parts to the ask and one of them could be described as a resettable lazy (in part... I'd also like to not worry about manually resetting the lazy but rather let the lazy reset itself as input changes).

For the other half of the ask, there are time where the laziness is not important and you explicitly don't want the lazy part of it at all:

let box = Box(height: 2, width: 4)
print(box.area) // Error: Cannot use mutating getter on immutable value: 'box' is a 'let' constant

Here the lazy property has prevented someone who wanted to use Box as a let constant from accessing the area (which just feels wrong in this example). When you you define any lazy property, you are making it clear that type is only intended to be used as a mutable type... and that's fine sometimes.

In the non-lazy version, the type can still be used as a let constant... my memoized "computed property" has effectively become a fixed stored property. But is still has the benefits of memoization for users who declare it as a mutable var variable.

So, the answer isn't one of the other, but both for different scenarios.

I like the looks of that.. what "resettable lazy" wrappers can't really do is watch values for change. (In general, this is a limitation of property wrappers, which really can't (and probably shouldn't) have this type of access to external properties). So, all very good and helpful stuff for improvements to lazy... but memoization based on dependency change is a slightly different can of worms.

I'll highlight this section of the more detailed pitch as the biggest real challenge here: what to do about reference types. Since a type really only contains the reference pointer, it's quite hard to monitor for any changes. If anyone has ideas, it would be very helpful.

React Hooks use that approach to use functional components instead of class components. So does Jetpack Compose in the Android world because all UI parts are functions but they need to manage state and re-rendering. In SwiftUI it's about structures and @State/@StateObject/@ObservedObject property wrappers

That's not how a computed property is supposed to work. Instead of worrying about recalculating whenever one of the dependencies changes, just recalculate it at the next access.

This seems like something you should be able to do with a property wrapper, I'm just not sure how you would observe the dependent properties.

I thought that too, but current property wrapper's spec can't do enclosing self in struct.
that is only available class type.

In my view, to compute and memoize a value from values inside a struct, we need something new language level function.

2 Likes

You don't really have to call it a "computed property". A memoized property is basically both computed and stored. As you said, the lazy version would compute on next access. But, since it has to store (mutate) on access, the lazy version can never be used with let constant. So, for types that could be let constants (but less concerned with lazy), you would need to recalculate on dependency change (or if let constant, just never recalculate).

Could we do a struct that wraps a class, like Array/Set/``Dictionary` do?

2 Likes

The problem with using a PropertyWrapper isn't that it's a struct. It's that it's initialized by the class or struct that uses it. In order to have a reference, the class or struct would have to pass self. However, it has to do so before it's fully initialized itself. To work, PropertyWrapper would have to be able to wait until it's caller was fully-initialized before being initialized.

This feels like a problem PropertyWrappers should be able to solve at first, but the more I've looked at it, the more I feel that this is really a different sort of issue. PropertyWrapper are really great for self-contained logic around a property. However, they really aren't designed to manage their caller. A PropertyWrapper solution for this feels like opening the door to reference cycles, etc.

I really tried to think of every way I could to make this as a PropertyWrapper, but it just seems like it's not the right tool for the job. I came to the conclusion that a better solution was to rely on property observers. This still doesn't quite work on reference type properties. However, I wonder if we could use a similar approach to Combine... so a dependent reference-type would have to implement ObservableObject and publish the needed properties to work with memoization. (This sort of revives the use of PropertyWrappers but on the other side of the equation).

It's just an information for this topic.
I've created a flux library(Verge) and this supports Computed properties with Memoization inside a state structure.

This has been built using a class object that contains the latest state and the previous state.
Through the declaration of Computed property, it returns a computed value that value could be that already computed if its dependencies not changed.

the syntax is like the following:

struct MyState: Verge.ExtendedStateType {

  var name: String = ...
  var items: [Int] = []

  struct Extended: Verge.ExtendedType {

    static let instance = Extended()

    let filteredArray = Field.Computed<[Int]> {
      $0.items.filter { $0 > 300 }
    }
    .dropsInput {
      $0.noChanges(\.items)
    }
  }
}
let store: Verge.Store<MyState, Never> = ...

let state: Verge.Changes<MyState> = store.state

let result: [Int] = state.computed.filteredArray

Verge.Changes contains the latest state and the previous state.

If you're curious, please try to read this document:

2 Likes

Very cool... I'll have to check this out!

Thanks for reading.
And so you know, that approach Verge uses several issues(weakness points).
the syntax of the definition that is not clear and smart since using proxy types to get a syntax like property accessing by dynamicMemberLookup.
That is to get syntax as possible as natural.

In my view, it would be nice if Swift supports memorized computed property in language level.

1 Like

Totally agree! I'll need to dig a bit deeper when I have the time, but I think there could be a lot of good building blocks in what you're doing to bring memoization to the language level. Please feel free to make some suggestions!

I think it highlights the basic issue: memoization is very doable with the language today, just not simple or pretty. I think most of the tools to get this done are already in front of us. All that's really needed are a few behind-the-scenes shortcuts and syntactic sugar.

Is your solution able to deal with changes to reference types? That seems to be the biggest challenge.

Terms of Service

Privacy Policy

Cookie Policy