3
\$\begingroup\$

First question here on CodeReview. I was directed here from a question I asked on SO:Swift protocol with lazy property - Cannot use mutating getter on immutable value: '$0' is immutable.

Goal: Create a protocol that allows lazy computation of a property for structs that conform to the protocol. The computation is intensive and should only be executed once, hence the lazy requirement.

So, after lots of reading (egSwift Struct with Lazy, private property conforming to Protocol, where I based this code on) and trial and error, I came up with something that works:

import Foundationprotocol Foo {    var footype: Double { mutating get }    func calculateFoo() -> Double}struct Bar: Foo {    private lazy var _footype: Double = {        let value = calculateFoo()        return value    }()    var footype: Double {        mutating get {            return _footype        }    }    func calculateFoo() -> Double {        print("calc")        return 3.453    }}

Testing this in a Playground:

var bar = Bar()print(bar.footype)print(bar.footype)

And the output is:

calc3.4533.453

So it is working. But it feels like a hack using_footype

Also in the comments, it was suggested that

However, I would not put calculateFoo in the protocol because it sounds like an implementation detail.

Appreciate any comments and/or improvements.

Martin R's user avatar
Martin R
24.2k2 gold badges38 silver badges96 bronze badges
askedOct 13, 2019 at 16:17
koen's user avatar
\$\endgroup\$
1
  • 1
    \$\begingroup\$Welcome to Code Review!\$\endgroup\$CommentedOct 13, 2019 at 17:19

1 Answer1

2
\$\begingroup\$

You don't need the additional_footype variable and a computer “wrapper” property. The protocol requirement can be satisfied with a lazy stored property:

struct Bar: Foo {    lazy var footype = calculateFoo()    func calculateFoo() -> Double {        print("calc")        return 3.453    }}

Nowfootype is read-only for instances of typeFoo

var foo: Foo = Bar()print(foo.footype)foo.footype = 12.3 // Error: Cannot assign to property: 'footype' is a get-only property

but as a property ofBar it is read-write:

var bar = Bar()print(bar.footype)bar.footype = 12.3 // OK

If assigning to thefootype property should be inhibited then you can mark it with aprivate(set) access modifier:

struct Bar: Foo {    private(set) lazy var footype = calculateFoo()    func calculateFoo() -> Double {        print("calc")        return 3.453    }}var bar = Bar()print(bar.footype)bar.footype = 12.3 // Cannot assign to property: 'footype' setter is inaccessible

With respect to

However, I would not put calculateFoo in the protocol because it sounds like an implementation detail.

Yes, in your current code it is an implementation detail of theBar class. The only use would be that a caller can “enforce” the evaluation:

var foo: Foo = Bar()foo.calculateFoo()

The situation would be different if there were a way to provide a default implementation in an extension method:

protocol Foo {    var footype: Double { mutating get }    func calculateFoo() -> Double}extension Foo {    var footype: Double {        // Call calculateFoo() on first call only ...    }}

so that conforming to the protocol is sufficient, i.e.Bar only has to implement thecalculateFoo() method, but not thefootype property.

But I am not aware of a way to do thislazily so that the function is called only once. The reason is that extensions cannot add stored properties to a type.

For subclasses ofNSObject you can come around this problem with “associated objects,” but not for structs.

answeredOct 13, 2019 at 17:01
Martin R's user avatar
\$\endgroup\$
5
  • \$\begingroup\$Yes,footype is calculated only, never set. So, just to be sure, to be able to use thelazy property, I need to usemutating invar footype: Double { mutating get } in the protocol?\$\endgroup\$CommentedOct 13, 2019 at 17:09
  • 1
    \$\begingroup\$@Koen: Yes (unless you make Foo a “class-only” protocol:protocol Foo: class {...} andBar a class instead of a struct).\$\endgroup\$CommentedOct 13, 2019 at 17:15
  • \$\begingroup\$yes, it has to be a struct, due to other parts of my code.\$\endgroup\$CommentedOct 13, 2019 at 17:19
  • \$\begingroup\$And the implementation ofcalculateFoo() is different for the various structs that conform toFoo, so they must all implement it for their specific case.\$\endgroup\$CommentedOct 13, 2019 at 17:39
  • \$\begingroup\$There is no need to wrapcalculateFoo() call into closure. You can writelazy var footype: Double = calculateFoo(), and it will work as expected,calculateFoo will be called just once, and only when the variable is accessed for the first time.\$\endgroup\$CommentedOct 31, 2019 at 0:48

You mustlog in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.