State within async closure not maintained

I'm trying to replicate a pattern in other languages where I maintain state inside a function which creates and returns a function immediately invoked.

func long_time() async -> String? {
    var start_with = 0
    return await { () async -> String in
        try? await Task.sleep(for: .seconds(2))
        start_with += 1
        return "\(start_with) \(Date.now)"
    }()
}


let channel = AsyncChannel<String>()

Task {
  while let result = await long_time() {
    await channel.send(result)
  }

  channel.finish()
}

for await calculationResult in channel {
  print(calculationResult)
}

but on print out , start_with always gets a value of 1 while date is indeed updating. So im not clear what's happening and why start_with doesn't increment each time successfully.

Can you clarify what you're expecting? e.g. perhaps post what output you're currently seeing and what output you want to see.

I get output

1 <the date> 
1 <the date>
1 <the date>

but I expected

1 <the_date>
2 <the_date>
3 <the_date>

Ah, you're expecting start_with to be preserved across calls. But it's just a local function variable. It lasts for only as a long as long_time does on each execution.

There's various ways to get the behaviour you want. I cannot tell, without further context, which is most appropriate for you. But here's one as an example:

import AsyncAlgorithms
import Foundation

func long_time_generator() -> () async -> String? {
    var start_with = 0
    return {
        try? await Task.sleep(for: .seconds(2))
        start_with += 1
        return "\(start_with) \(Date.now)"
    }
}

let channel = AsyncChannel<String>()

Task {
    let long_time = long_time_generator()
    while let result = await long_time() {
        await channel.send(result)
    }

    channel.finish()
}

for await calculationResult in channel {
    print(calculationResult)
}

The difference is that it creates a persistent closure that you then invoke repeatedly. The closure captures start_with, persisting its value across calls.

I understand it's a local var - however in many other languages this exact pattern is done for functions that maintain local state - so why is swift deviating from others in this case, what is the reasoning

okay I see - because I immediately invoked it while you have it returning the func itself, yes that was my mistake

What you've written won't work in other languages either (at least, any that I'm aware of). You might be thinking of a variation where you make start_with static in C-family languages. Swift doesn't have that specific syntax (because it's a foot-gun, among other reasons - it often leads to concurrency-unsafe code).

You could move start_with up to global level, that'd be your nearest analogy. I wouldn't, though - globals are inherently serialising, in the sense that if you then have two users of long_time concurrently, they'll share the one start_with global and that's presumably not what you want here. It also wouldn't be concurrency-safe as-is - you'd really need to use Atomics for the value if you genuinely want to share and increment it across multiple isolation domains.

Are you looking for this?

func long_time() -> () async -> String {
    var start_with = 0
    return {
        try? await Task.sleep(for: .seconds(1))
        start_with += 1
        return "\(start_with) \(Date.now)"
    }
}

Task {
    let proc = long_time()
    await print(proc()) // 1
    await print(proc()) // 2
    await print(proc()) // 3
}
1 Like

yes I made the mistake of invoking the func immediately