Skip to content

Releases: 0xLeif/Fork

2.0.0

20 Sep 02:21
31d3168
Compare
Choose a tag to compare

What's Changed

Full Changelog: 1.3.0...2.0.0

1.3.0

23 Jun 19:43
58c39d9
Compare
Choose a tag to compare

BatchedForkedArray

The BatchedForkedArray allows you to efficiently parallelize and batch process an array of values using an async function. It provides methods for both resolving the parallelized array in a single output as well as streaming the batches of the resolved array.

let batchedForkedArray = BatchedForkedArray(photoNames, batch: 3, map: downloadPhoto(named:))
let photos = try await forkedArray.output()

In the above example, we create an instance of BatchedForkedArray with a batch size of 3 and the downloadPhoto function as the map closure.

To resolve the batched array, we use the output() method, which executes the downloadPhoto function on each batch of photo names in parallel. After the resolution is complete, the photos array will contain the downloaded photos in the order they were processed.

let photoNames = [Int](0 ..< 100)

let batchedForkedArray = BatchedForkedArray(
    photoNames,
    batch: 5,
    map: downloadPhoto(named:)
)

for try await batch in batchedForkedArray.stream() {
    for photo in batch {
        // Perform operations on each photo in the batch
        print(photo)
    }
}

In this example, we create an instance of BatchedForkedArray with a batch size of 5 and the downloadPhoto(named:) function as the map closure. By using the stream() method, we can iterate over batches of photo names asynchronously.

Within the for-await loop, each batch of photo names is processed asynchronously. We then iterate over each photo in the batch and perform operations accordingly. This allows for efficient processing of large datasets in batches while controlling the number of parallel processes running at once.


What's Changed

  • Add BatchedForkedArray and other changes by @0xLeif in #17

Full Changelog: 1.2.0...1.3.0

1.2.0

22 Sep 01:03
7682617
Compare
Choose a tag to compare

What's Changed

Full Changelog: 1.1.0...1.2.0

1.1.0

16 Sep 03:10
d2ab6bf
Compare
Choose a tag to compare

What's Changed

Full Changelog: 1.0.1...1.1.0

1.0.1

23 Aug 03:16
8fff92e
Compare
Choose a tag to compare

What's Changed

  • Add async and throwing capability to value closure for Fork by @0xLeif in #12
  • Remove await and prefer parallel await using async let for ForkedArray by @0xLeif in #12

Full Changelog: 1.0.0...1.0.1


Fork

Parallelize two or more async functions

What is Fork?

Fork allows for a single input to create two separate async functions that return potentially different outputs. Forks can also merge their two functions into one which returns a single output.

The word "fork" has been used to mean "to divide in branches, go separate ways" as early as the 14th century. In the software environment, the word evokes the fork system call, which causes a running process to split itself into two (almost) identical copies that (typically) diverge to perform different tasks.
Source

Why use Fork?

Swift async-await makes it easy to write more complicated asynchronous code, but it can be difficult to parallelize multiple functions.

The Swift Book has the following example for downloading multiple images.

let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])

let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

Now the code above is still asynchronous, but will only run one function at a time. In the example above, firstPhoto will be set first, then secondPhoto, and finally thirdPhoto.

To run these three async functions in parallel we need to change the code to this following example.

async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])

let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

The above code will now download all three photos at the same time. When all the photos have been downloaded it will show the photos.

This is a simple async-await example of running code in parallel in which you might not need to use Fork. More complicated examples though might require async dependencies. For example what if we needed to authenticate with a server; then use the auth token to download the photos while also fetching some data from the database. This is where Fork is useful!

When using Fork, functions will be ran in parallel and higher order forks will also be ran in parallel.

Objects

  • Fork: Using a single input create two separate async functions that return LeftOutput and RightOutput.
  • ForkedActor: Using a single actor create two separate async functions that are passed the actor.
    • KeyPathActor: A generic Actor that uses KeyPaths to update and set values.
  • ForkedArray: Using a single array and a single async function, parallelize the work for each value of the array.

Basic usage

import Fork

Fork Example

let fork = Fork(
    value: 10,
    leftOutput: { $0.isMultiple(of: 2) },
    rightOutput: { "\($0)" }
)
        
let leftOutput = try await fork.left()
let rightOutput = try await fork.right()

XCTAssertEqual(leftOutput, true)
XCTAssertEqual(rightOutput, "10")
        
let mergedFork: () async throws -> String = fork.merge(
    using: { bool, string in
        if bool {
            return string + string
        }
            
        return string
    }
)
        
let output = await mergedFork()

XCTAssertEqual(output, "1010")

ForkedActor Example

actor TestActor {
    var value: Int = 0
    
    func increment() {
        value += 1
    }
}

let forkedActor = ForkedActor(
    actor: TestActor(),
    leftOutput: { actor in
        await actor.increment()
    },
    rightOutput: { actor in
        try await actor.fork(
            leftOutput: { await $0.increment() },
            rightOutput: { await $0.increment() }
        )
        .act()
    }
)

let actorValue = await forkedActor.act().value

XCTAssertEqual(actorValue, 3)

ForkedActor KeyPathActor Example

let forkedActor = ForkedActor(
    value: 0,
    leftOutput: { actor in
        await actor.update(to: { $0 + 1 })
    },
    rightOutput: { actor in
        try await actor.fork(
            leftOutput: { actor in
                await actor.update(to: { $0 + 1 })
            },
            rightOutput: { actor in
                await actor.update(\.self, to: { $0 + 1 })
            }
        )
        .act()
    }
)

let actorValue = try await forkedActor.act().value

XCTAssertEqual(actorValue, 3)

ForkedArray Example

A ForkedArray makes it easy to perform an asynchronous function on all of the elements in an Array. ForkedArray helps with the example above.

let forkedArray = ForkedArray(photoNames, output: downloadPhoto(named:))
let photos = try await forkedArray.output()

Extra Examples

Vapor ForkedActor Example

Service Example

let service = Fork(
    value: AppConfiguration(),
    leftOutput: { configuration in
        Fork(
            value: AuthService(configuration),
            leftOutput: { authService in ... },
            rightOutput: { authService in ... }
        )
    },
    rightOutput: { configuration in
        ...
    }
)

let mergedServiceFork: async throws () -> AppServices = service.merge(
    using: { authFork, configurationOutput in
        let services = try await authFork.merged(...)
            
        services.logger.log(configurationOutput)
            
        return services
    }
)

1.0.0

18 Aug 23:32
ea62ba5
Compare
Choose a tag to compare

What's Changed

  • Added KeyPathActor and Release/1.0.0 by @0xLeif in #11

Full Changelog: 0.7.0...1.0.0


Fork

Parallelize two or more async functions

What is Fork?

Fork allows for a single input to create two separate async functions that return potentially different outputs. Forks can also merge their two functions into one which returns a single output.

The word "fork" has been used to mean "to divide in branches, go separate ways" as early as the 14th century. In the software environment, the word evokes the fork system call, which causes a running process to split itself into two (almost) identical copies that (typically) diverge to perform different tasks.
Source

Why use Fork?

Swift async-await makes it easy to write more complicated asynchronous code, but it can be difficult to parallelize multiple functions.

The Swift Book has the following example for downloading multiple images.

let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])

let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

Now the code above is still asynchronous, but will only run one function at a time. In the example above, firstPhoto will be set first, then secondPhoto, and finally thirdPhoto.

To run these three async functions in parallel we need to change the code to this following example.

async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])

let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

The above code will now download all three photos at the same time. When all the photos have been downloaded it will show the photos.

This is a simple async-await example of running code in parallel in which you might not need to use Fork. More complicated examples though might require async dependencies. For example what if we needed to authenticate with a server; then use the auth token to download the photos while also fetching some data from the database. This is where Fork is useful!

When using Fork or ForkedActor, both functions will be ran in parallel. Higher order forks will also be ran in parallel.

Basic usage

import Fork

Fork Example

let fork = Fork(
    value: 10,
    leftOutput: { $0.isMultiple(of: 2) },
    rightOutput: { "\($0)" }
)
        
let leftOutput = try await fork.left()
let rightOutput = try await fork.right()

XCTAssertEqual(leftOutput, true)
XCTAssertEqual(rightOutput, "10")
        
let mergedFork: () async throws -> String = fork.merge(
    using: { bool, string in
        if bool {
            return string + string
        }
            
        return string
    }
)
        
let output = await mergedFork()

XCTAssertEqual(output, "1010")

ForkedActor Example

actor TestActor {
    var value: Int = 0
    
    func increment() {
        value += 1
    }
}

let forkedActor = ForkedActor(
    actor: TestActor(),
    leftOutput: { actor in
        await actor.increment()
    },
    rightOutput: { actor in
        try await actor.fork(
            leftOutput: { await $0.increment() },
            rightOutput: { await $0.increment() }
        )
        .act()
    }
)

let actorValue = await forkedActor.act().value

XCTAssertEqual(actorValue, 3)

ForkedActor KeyPathActor Example

let forkedActor = ForkedActor(
    value: 0,
    leftOutput: { actor in
        await actor.update(to: { $0 + 1 })
    },
    rightOutput: { actor in
        try await actor.fork(
            leftOutput: { actor in
                await actor.update(to: { $0 + 1 })
            },
            rightOutput: { actor in
                await actor.update(\.self, to: { $0 + 1 })
            }
        )
        .act()
    }
)

let actorValue = try await forkedActor.act().value

XCTAssertEqual(actorValue, 3)

ForkedArray Example

A ForkedArray makes it easy to perform an asynchronous function on all of the elements in an Array. ForkedArray helps with the example above.

let forkedArray = ForkedArray(photoNames, output: downloadPhoto(named:))
let photos = try await forkedArray.output()

Extra Examples

Vapor ForkedActor Example

Service Example

let service = Fork(
    value: AppConfiguration(),
    leftOutput: { configuration in
        Fork(
            value: AuthService(configuration),
            leftOutput: { authService in ... },
            rightOutput: { authService in ... }
        )
    },
    rightOutput: { configuration in
        ...
    }
)

let mergedServiceFork: async throws () -> AppServices = service.merge(
    using: { authFork, configurationOutput in
        let services = try await authFork.merged(...)
            
        services.logger.log(configurationOutput)
            
        return services
    }
)

0.7.0

18 Aug 03:47
78dfa23
Compare
Choose a tag to compare

What's Changed

  • Add async map and filter to array by @0xLeif in #10

Full Changelog: 0.6.3...0.7.0

ForkedArray Examples

let photoNames: [String] = ...
func isValidPhoto(named: String) async -> Bool { ... }
func downloadPhoto(named: String) async -> Photo { ... }

ForkedArray init

let forkedArray: ForkedArray<String, Photo> = ForkedArray(
    photoNames,
    filter: isValidPhoto(named:),
    map: downloadPhoto(named:)
)
let photos: [Photo] = try await forkedArray.output()

Array ForkedArray init

let forkedArray = photoNames.fork
    filter: isValidPhoto(named:),
    map: downloadPhoto(named:)
)
let photos: [Photo] = try await forkedArray.output()

Array asyncFilter

let photoNames: [String] = try await photoNames.asyncFilter(isValidPhoto(named:))

Array asyncMap

let photos: [Photo] = try await photoNames.asyncMap(downloadPhoto(named:))

0.6.3

18 Aug 02:31
Compare
Choose a tag to compare

What's Changed

  • Added better Task cancellation for ForkedArray in 6fe18c3

Full Changelog: 0.6.2...0.6.3

0.6.2

18 Aug 01:32
9c15c66
Compare
Choose a tag to compare

What's Changed

Full Changelog: 0.6.1...0.6.2

0.6.1

18 Aug 01:05
6f0f1c1
Compare
Choose a tag to compare

What's Changed

Full Changelog: 0.6.0...0.6.1