{"id":20642693,"url":"https://github.com/typelift/abstract","last_synced_at":"2025-04-16T01:44:20.098Z","repository":{"id":63921174,"uuid":"93772847","full_name":"typelift/Abstract","owner":"typelift","description":"Practical Abstract Algebra in Swift","archived":false,"fork":false,"pushed_at":"2021-08-27T05:33:10.000Z","size":5797,"stargazers_count":47,"open_issues_count":1,"forks_count":5,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-09T23:41:29.206Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/typelift.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-06-08T17:00:13.000Z","updated_at":"2024-02-04T05:54:15.000Z","dependencies_parsed_at":"2023-01-14T14:15:38.254Z","dependency_job_id":null,"html_url":"https://github.com/typelift/Abstract","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/typelift%2FAbstract","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/typelift%2FAbstract/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/typelift%2FAbstract/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/typelift%2FAbstract/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/typelift","download_url":"https://codeload.github.com/typelift/Abstract/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249098096,"owners_count":21212421,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-16T16:09:55.592Z","updated_at":"2025-04-16T01:44:20.069Z","avatar_url":"https://github.com/typelift.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Abstract\n\nA take on abstract algebraic structures, in Swift. \n\n------\n\n__Please note:__ this is deprecated and no longer maintained. I'm working on a new take on abstract algebra in Swift with [swift-abstract](https://github.com/broomburgo/swift-abstract).\n\n------\n\n`Abstract` is a Swift library that defines protocols for common [abstract algebraic structures](https://en.wikipedia.org/wiki/Abstract_algebra), along with some concrete implementations for Swift datatypes.\n\nThe library also provides tools to test the concrete types for the axioms required by each algebraic structure: tests can then be performed by property-based testing libraries like [SwiftCheck](https://github.com/typelift/SwiftCheck).\n\n------\n\n## System Requirements\n\n`Abstract` supports macOS 10.10+ and iOS 8.3+.\n\n------\n\n## Setup\n\nTo clone `Abstract` please run `git clone REPOSITORY_URL --recursive` to properly clone submodules.\n\n### SwiftPM\nPlease add this line to your `Package.swift` file's dependencies section:\n\n```\n.package(url: \"https://github.com/typelift/Abstract.git\",\n         from: Version(0,0,0))\n```\n\nTo use the structures in this library, add `\"Abstract\"` to your target's dependencies. To additionally test algebraic laws with the framework, add `\"Abstract\"` as a dependency to the relevant `testTarget`s.\n\n### Carthage\n`Abstract` is compatible with [Carthage](https://github.com/Carthage/Carthage): please refer to Carthage documentation for how to add `Abstract` as a dependency of your project.\n\n------\n\n## How to use this\n\nLet's see some examples to better understand what `Abstract` is about. To get an overview of the theory behind all this you can read the [rationale](RATIONALE.md).\n\n### Cookies appreciation\n\nWe're building an app that lets a user request any number of cookies, and then provide a feedback if they're satisfied with the cookies or not. We'd like to track the user interaction with the app in a session object like this:\n\n```swift\nstruct UserSession {\n    var lastInteractionDate: Date\n    var numberOfCookiesRequested: Int\n    var maxCookiesPerRequest: Int\n    var averageCookiesPerSession: Double\n    var alwaysSatisfied: Bool\n}\n```\n\nAt each interaction, we should update the session object. \n\nThere are 2 possible interactions:\n\n- order the cookies;\n- leave a feedback;\n\nWe could add 2 methods to `UserSession` to represent each of those actions:\n\n```swift\nextension UserSession {\n    func orderCookies(_ count: Int) -\u003e UserSession {\n        var m = self\n        m.lastInteractionDate = Date()\n        m.totalCookiesRequested += count\n        m.maxCookiesPerRequest = max(m.maxCookiesPerRequest, count)\n//        m.averageCookiesPerRequest = ?\n        return m\n    }\n    \n    func leaveFeedback(_ positive: Bool) -\u003e UserSession {\n        var m = self\n        m.lastInteractionDate = Date()\n        m.alwaysSatisfied = m.alwaysSatisfied \u0026\u0026 positive\n        return m\n    }\n}\n```\n\nWe notice 2 things:\n\n- each property updates following a specific logic that's not explicitly declared, but must be deduced from the context;\n- there's no way we can update the `averageCookiesPerRequest` property without keeping track of the total number of requests (something that we don't care about from a business perspective);\n\nWe could try and define additional types that explicitly declare how we're supposed to update the various properties.\n\n```swift\nvar lastInteractionDate: Max\u003cDate\u003e\nvar totalCookiesRequested: Add\u003cInt\u003e\nvar maxCookiesPerRequest: Max\u003cInt\u003e\nvar alwaysSatisfied: And\n```\n\nEach property is of a type that specifies how we're supposed to *compose* two instances: `Max\u003cDate\u003e` will always keep the highest of two dates, and it's going to be the same for `Max\u003cInt\u003e` but for numbers; `Add\u003cInt\u003e` will compose the numbers by adding them, and `And` will apply the `\u0026\u0026` operation to two `Bool`. We can then get the *wrapped* value inside the type with an `unwrap` function.\n\nFollowing this strategy we can actually define an `Average` type that declares a composition function that works as intended:\n\n```swift\nstruct Average {\n    private var value: Double\n    private var weight: Int\n    \n    var unwrap: Double {\n        return value\n    }\n    \n    init(_ value: Double, weight: Int = 1) {\n        self.value = value\n        self.weight = weight\n    }\n    \n    static func \u003c\u003e (lhs: Average, rhs: Average) -\u003e Average {\n        let sum = lhs.value*Double(lhs.weight) + rhs.value*Double(rhs.weight)\n        let count = lhs.weight + rhs.weight\n        return Average.init(sum/Double(count), weight: count)\n    }\n}\n```\n\nNotice that we're using a `\u003c\u003e` operator instead of a `compose` method.\n\nNow we can redefine our `UserSession` like this:\n\n```swift\nstruct UserSession {\n    var lastInteractionDate: Max\u003cDate\u003e\n    var totalCookiesRequested: Add\u003cInt\u003e\n    var maxCookiesPerRequest: Max\u003cInt\u003e\n    var averageCookiesPerRequest: Average\n    var alwaysSatisfied: And\n}\n\nextension UserSession {\n    func orderCookies(_ count: Int) -\u003e UserSession {\n        var m = self\n        m.lastInteractionDate = m.lastInteractionDate \u003c\u003e Max(Date())\n        m.totalCookiesRequested = m.totalCookiesRequested \u003c\u003e Add(count)\n        m.maxCookiesPerRequest = m.maxCookiesPerRequest \u003c\u003e Max(count)\n        m.averageCookiesPerRequest = m.averageCookiesPerRequest \u003c\u003e Average(Double(count))\n        return m\n    }\n    \n    func leaveFeedback(_ positive: Bool) -\u003e UserSession {\n        var m = self\n        m.lastInteractionDate = m.lastInteractionDate \u003c\u003e Max(Date())\n        m.alwaysSatisfied = m.alwaysSatisfied \u003c\u003e And(positive)\n        return m\n    }\n}\n```\n\nThis looks cool but boring: thanks to the fact that each of our types composes with `\u003c\u003e`, we're repeating what's basically the same operation over and over again. It would probably be better to extend the composition operation to `UserSession` itself, where we compose 2 sessions by composing each pair of properties.\n\n```swift\nextension UserSession {\n    static func \u003c\u003e (lhs: UserSession, rhs: UserSession) -\u003e UserSession {\n        return UserSession.init(\n            lastInteractionDate: lhs.lastInteractionDate \u003c\u003e rhs.lastInteractionDate,\n            totalCookiesRequested: lhs.totalCookiesRequested \u003c\u003e rhs.totalCookiesRequested,\n            maxCookiesPerRequest: lhs.maxCookiesPerRequest \u003c\u003e rhs.maxCookiesPerRequest,\n            averageCookiesPerRequest: lhs.averageCookiesPerRequest \u003c\u003e rhs.averageCookiesPerRequest,\n            alwaysSatisfied: lhs.alwaysSatisfied \u003c\u003e rhs.alwaysSatisfied)\n    }\n}\n```\n\nNotice that we're simply merging the properties in pairs: this could actually be defined in a completely generic way, either with a generic `Tuple` struct where the properties can be *combined*, or with a code-generation tool like [Sourcery](https://github.com/krzysztofzablocki/Sourcery).\n\nNow our `orderCookies` and `leaveFeedback` methods can actually be redefined as `static` methods that generate the *new* session to be combined with the previous one. To do that we need to be able to generate *empty* values for the properties, that are going to behave to the composition like *zero* behaves to *addition*, that is, it leaves the previous value unchanged.\n\n```swift\nextension UserSession {\n    static func orderCookies(_ count: Int) -\u003e UserSession {\n        return UserSession.init(\n            lastInteractionDate: Max(Date()),\n            totalCookiesRequested: Add(count),\n            maxCookiesPerRequest: Max(count),\n            averageCookiesPerRequest: Average(Double(count)),\n            alwaysSatisfied: .empty)\n    }\n    \n    static func leaveFeedback(_ positive: Bool) -\u003e UserSession {\n        return UserSession.init(\n            lastInteractionDate: Max(Date()),\n            totalCookiesRequested: .empty,\n            maxCookiesPerRequest: .empty,\n            averageCookiesPerRequest: .empty,\n            alwaysSatisfied: And(positive))\n    }\n}\n```\n\nNow a bunch of interactions can be easily combined like this:\n\n```swift\nlet finalSession: UserSession = .orderCookies(3)\n    \u003c\u003e .orderCookies(5)\n    \u003c\u003e .leaveFeedback(true)\n    \u003c\u003e .orderCookies(2)\n    \u003c\u003e .orderCookies(1)\n    \u003c\u003e .orderCookies(4)\n    \u003c\u003e .leaveFeedback(true)\n    \u003c\u003e .orderCookies(10)\n    \u003c\u003e .leaveFeedback(false)\n    \nlet totalCookiesRequested = finalSession.totalCookiesRequested.unwrap // 25\nlet maxCookiesPerRequest = finalSession.maxCookiesPerRequest.unwrap // 10\nlet averageCookiesPerRequest = finalSession.averageCookiesPerRequest.unwrap // 4.167\nlet alwaysSatisfied = finalSession.alwaysSatisfied.unwrap // false\n```\n\nIf we provide an `.empty` value also for `UserSession` we can actually collect all the interactions in an `Array`, and then `reduce` the collection. This is definitely more convenient and readable, and allows us to separate the *collection* of the data from their *processing*. `UserSession.empty` will naturally be an instance where every property is `.empty`:\n\n```swift\nlet sessions: [UserSession] = [\n    .orderCookies(3),\n    .orderCookies(5),\n    .leaveFeedback(true),\n    .orderCookies(2),\n    .orderCookies(1),\n    .orderCookies(4),\n    .leaveFeedback(true),\n    .orderCookies(10),\n    .leaveFeedback(false)\n]\n\nlet finalSession = sessions.reduce(.empty, \u003c\u003e)\n```\n\nNotice that we could call `.reduce(.empty, \u003c\u003e)` on **any** collection were the elements have these two properties:\n\n- can be composed with `\u003c\u003e`;\n- have an empty element;\n\nThus, if we were able to represent these two properties in an abstract way, we could simply define a `.concatenated()` method for these kinds of collections:\n\n```swift\nlet finalSession = sessions.concatenated()\n```\n\nA type (actually a set, but in programming we really just care about types) *equipped* with a composition operation that is *closed* (i.e. non-crashing) and *associative*, and an `.empty` value that is neutral to the composition, is usually called a `Monoid`: all the types defined in this example are monoids, and the Swift type system is strong enough to generically define  the interface of a monoid with a `protocol`. Most of the types and methods used in this example are already defined in `Abstract`, and to read more about monoids you can refer to the [Monoid.swift](Sources/Abstract/Monoid.swift) source file.\n\n### FizzBuzzNess\n\n[FizzBuzz](http://wiki.c2.com/?FizzBuzzTest) is a classic job interview question used to check the way a candidate approaches the resolution of a problem in code.\n\nThe requirement is to write a program that, given a list of integers, prints *Fizz* for every number divisible by 3, \"Buzz\" for every number divisible by 5, \"FizzBuzz\" for every number divisible by both (thus, divisible by 15), and the number itself when it's not divisible by any. It's an easy problem to solve in Swift with a `for-in` cycle and a couple of `if-else` statements, but then the interview could proceed by asking the candidate how to make the solution more generic, by removing duplicate logic in the checks for divisibility by 3 and/or 5, and scalable, so that it's easy for example to introduce a \"Bazz\" word when the number is divisible by 4, that should be combined appropriately with the other 2 words (thus, when the number is divisible by 12, 20 or 60).\n\nThis kind of problem can be elegantly solved with some abstract algebra. The fundamental intuition behind a possible abstract algebra approach is that we're dealing with *putting things together* in various ways.\n\nLet's call \"special divisors\" the numbers associated to each word (initially, 3 for \"Fizz\" and 5 for \"Buzz\"). Every integer could have any number of special divisors, including none, so we're dealing with two kinds of composition:\n\n- words like \"Fizz\" and \"Buzz\" should concatenate when a number has more than one special divisor;\n- the way special words and the number itself concatenate is that the number is ignored when the special word exists, so the latter has priority;\n\nThe first composition style is simple concatenation; the second one is a little harder to see as some kind of composition, but it actually is the composition where we get only the first value if it exists (even if both exist), otherwise we get the second, and if none exist we get an \"empty\" value.\n\nThe type representing the string concatenation is simply `String`, which naturally forms a monoid over concatenation, where the `.empty` value is just the empty string.\n\nAbout the simple string concatenation, we'd like to define a function that *associates* a *word* to a special divisor: the function will take an `Int` and return a `String`, which is going to be \"Fizz\" or \"Buzz\". But instead of concatenating words we would actually like to concatenate *functions* that return words: if we're able to compose the return value, we can actually define a *composable function*:\n\n```swift\nfunc associate(divisor: Int, to text: String) -\u003e Function\u003cInt,String\u003e {\n    return Function.init { value in\n        value % divisor == 0 ? text : .empty\n    }\n}\n```\n\nThe `Function` type is a *function type* (we get the function back with the `.call` method) that's **also** a monoid, so we can compose and concatenate instances of this function like we'd do for `String` values.\n\nWe can easily define our `fizz` and `buzz` associations:\n\n```swift\nlet fizz = associate(divisor: 3, to: \"Fizz\")\nlet buzz = associate(divisor: 5, to: \"Buzz\")\n```\n\nNow we can easily generate a function that will transform a number in a word, properly concatenated (like \"FizzBuzz\" for the number 15), or an empty string if the number has no special divisor.\n\n```swift\nlet transform = [fizz, buzz].concatenated().call\n```\n\n For the second type of composition, Swift already provides a type with the correct semantics; we need to give priority to the *first* element, but only if it's not `.empty`, otherwise we yield the second value (`.empty` or not): that's exactly the composition semantics of `Optional`, where `.empty` is `.none` (or `nil`) and the composition operation is represented by the `??` operator. `Abstract` extends `Optional` with the `Monoid` protocol, adding the `.empty` instance and the `\u003c\u003e` operator.  We can define a `getWord` function that will use `Optional\u003cString\u003e` to select a value in a composition:\n\n```swift\nfunc getWord\u003cT\u003e(for value: T, with association: @escaping (T) -\u003e String) -\u003e String {\n\tlet optionalAssociated = Optional(association(value))\n\t\t.flatMap { $0 == .empty ? Optional.empty : Optional($0) }\n\n\tlet optionalValue = Optional(\"\\(value)\")\n\n\treturn (optionalAssociated \u003c\u003e optionalValue) ?? \"\"\n}\n```\n\nWe can verify the result by putting things together:\n\n```swift\nlet result = (1...100)\n    .map { value in\n        getWord(for: value, with: transform).unwrap \u003c\u003e \"\\n\"\n    }\n    .concatenated()\n\nprint(result)\n```\n\nNow that we've separated the two kinds of composition that are taking place here, we can easily add more words and associations. For example:\n\n```swift\nlet bazz = associate(divisor: 4, to: \"Bazz\")\nlet transform = [fizz, buzz, bazz].concatenated().call\n``` \n\nThis code will add the word \"Bazz\" to the mix, for all numbers divisible by 4. Notice that in our example, for the number 60 the word \"FizzBuzzBazz\" will be printed: the order matters here, and we get \"Bazz\" at the end because we composed our transformation like `[fizz, buzz, bazz]`.\n\n### The order matters (not)\n\nLet's assume we have a `Process\u003cT\u003e` type that encapsulates a `run` function, executable without any input, that returns a value of type `T`.\n\n```swift\nstruct Process\u003cT\u003e {\n    let run: () -\u003e T\n}\n```\n\nLet's also assume we have a bunch of processes that we want to run, and then combine all the values into a single one. Running all the processes in sequence and then collecting all the values could be tedious and inefficient, but running them in parallel, maybe in a distributed way, could be dangerous, unpredictable and hard to coordinate.\n\nWe would like to take advantage of the abstract algebraic structures defined in `Abstract` to simplify the problem. Everything depends on the `T` value: it turns out that, if `T` has certain properties, we can actually run our processes in a distributed and efficient way without any risk.\n\nWe have a collection of these processes, and we'd like to run all of them by distributing computation to many units: the processes can require different times to complete, and we'd like to distribute our processing units to different threads/queues.\n\n```swift\nclass ProcessingUnit {\n    let context: Context\n    init(context: Context) {\n        self.context = context\n    }\n    \n    func execute\u003cT\u003e(_ process: Process\u003cT\u003e, onComplete: @escaping (T) -\u003e ()) {\n        context.execute {\n            let value = process.run()\n            onComplete(value)\n        }\n    }\n}\n```\n\nThe `ProcessingUnit` uses an execution context on which to run the process: we can represent this with a `protocol` and make `DispatchQueue` conform to it:\n\n```swift\nprotocol Context {\n    func execute(_ call: @escaping () -\u003e ())\n}\n\nextension DispatchQueue: Context {\n    func execute(_ call: @escaping () -\u003e ()) {\n        async {\n            call()\n        }\n    }\n}\n```\n\nA `Collector` class will receive all the `T` values and combine them together: the requirement on `T`, in this case, is for it to be a monoid, so it has an `.empty` value and a `\u003c\u003e` associative composition operation.\n\n```swift\nclass Collector\u003cT: Monoid\u003e {\n    private var current: T = .empty\n    func append(_ value: T) {\n        current = current \u003c\u003e value\n    }\n}\n```\n\nFinally, a `Distributor` class will distribute the work to processing units:\n\n```swift\nclass Distributor {\n    let serialContext = DispatchQueue.init(label: \"serial\")\n    \n    func distribute\u003cT\u003e(process: Process\u003cT\u003e, to collector: Collector\u003cT\u003e) {\n        ProcessingUnit.init(context: serialContext).execute(process, onComplete: collector.append)\n    }\n}\n```\n\nNotice that, if the only constraint that we impose on `T` is `Monoid` (in the code above the requirement is implicit) we cannot do much more than distributing the work on a serial queue, because we need the processes to run and complete in the same order as they're passed to the distributor.\n\nAn improvement over this would be if `T` was a `CommutativeMonoid`: in this case the `\u003c\u003e` operation is declared to be commutative, which means that the order of composition doesn't matter. This way we can distribute work over a concurrent queue: even if a process ends before one that started earlier, the commutativity will insure that the composition still makes sense.\n\n```swift\nclass Distributor {\n    let concurrentContext = DispatchQueue.init(label: \"concurrent\", attributes: .concurrent)\n    \n    func distribute\u003cT\u003e(process: Process\u003cT\u003e, to collector: Collector\u003cT\u003e) where T: CommutativeMonoid {\n        ProcessingUnit.init(context: concurrentContext).execute(process, onComplete: collector.append)\n    }\n}\n```\n\nTo make further improvements we could consider the case of actually distributed executions on stateless, uncoordinated contexts that could fail, restart and execute more than once. If `T` were a `BoundedSemilattice` we could actually make this kind of context work anyway because the composition operation, other than commutative, would be declared *idempotent*, that is, applying it twice would be the same as applying it once.\n\n```swift\nclass Unreliable: Context {\n    func execute(_ call: @escaping () -\u003e ()) {\n        /// could call more than once\n    }\n}\n\nclass Distributor {\n    let unreliableContext = Unreliable()\n    \n    func distribute\u003cT\u003e(process: Process\u003cT\u003e, to collector: Collector\u003cT\u003e) where T: BoundedSemilattice {\n        ProcessingUnit.init(context: unreliableContext).execute(process, onComplete: collector.append)\n    }\n}\n```\n\nBy clearly defining the composition semantics of `T` we can make assumptions that allow us to be more efficient and less constrained. But making `T` conform to `BoundedSemilattice`, `CommutativeMonoid` or even `Monoid` will require that the composition operation on `T` respects some laws like *commutativity*: `Abstract` provides functions, defined in the `Law` namespace, that will allow one to test a type against these laws, thus proving that the type has the required semantics.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftypelift%2Fabstract","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftypelift%2Fabstract","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftypelift%2Fabstract/lists"}