From 21cd15d7a259e3ada2401e2585455587a56bdfbd Mon Sep 17 00:00:00 2001 From: Amir Saeid Date: Sun, 8 Feb 2026 22:52:14 +0000 Subject: first commit --- .envrc | 1 + .github/workflows/ci.yml | 281 +++++++++++++++++++++ .github/workflows/clean.yml | 59 +++++ .gitignore | 25 ++ .scalafix.conf | 5 + .scalafmt.conf | 8 + CODE_OF_CONDUCT.md | 14 + LICENSE | 201 +++++++++++++++ NOTICE | 3 + README.md | 3 + build.sbt | 35 +++ .../main/scala/com/codiff/fairstream/Fair.scala | 60 +++++ .../main/scala/com/codiff/fairstream/FairT.scala | 77 ++++++ .../main/scala/com/codiff/fairstream/Main.scala | 26 ++ .../scala/com/codiff/fairstream/MainSuite.scala | 28 ++ docs/index.md | 3 + flake.nix | 27 ++ project/build.properties | 1 + project/plugins.sbt | 5 + 19 files changed, 862 insertions(+) create mode 100644 .envrc create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/clean.yml create mode 100644 .gitignore create mode 100644 .scalafix.conf create mode 100644 .scalafmt.conf create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 build.sbt create mode 100644 core/src/main/scala/com/codiff/fairstream/Fair.scala create mode 100644 core/src/main/scala/com/codiff/fairstream/FairT.scala create mode 100644 core/src/main/scala/com/codiff/fairstream/Main.scala create mode 100644 core/src/test/scala/com/codiff/fairstream/MainSuite.scala create mode 100644 docs/index.md create mode 100644 flake.nix create mode 100644 project/build.properties create mode 100644 project/plugins.sbt diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e29033a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,281 @@ +# This file was automatically generated by sbt-github-actions using the +# githubWorkflowGenerate task. You should add and commit this file to +# your git repository. It goes without saying that you shouldn't edit +# this file by hand! Instead, if you wish to make changes, you should +# change your sbt build configuration to revise the workflow description +# to meet your needs, then regenerate this file. + +name: Continuous Integration + +on: + pull_request: + branches: ['**', '!update/**', '!pr/**'] + push: + branches: ['**', '!update/**', '!pr/**'] + tags: [v*] + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + +concurrency: + group: ${{ github.workflow }} @ ${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Test + strategy: + matrix: + os: [ubuntu-22.04] + scala: [2.13, 3] + java: [temurin@8] + project: [rootJS, rootJVM] + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - name: Checkout current branch (full) + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup sbt + uses: sbt/setup-sbt@v1 + + - name: Setup Java (temurin@8) + id: setup-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 8 + cache: sbt + + - name: sbt update + if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' + run: sbt +update + + - name: Check that workflows are up to date + run: sbt githubWorkflowCheck + + - name: Check headers and formatting + if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' headerCheckAll scalafmtCheckAll 'project /' scalafmtSbtCheck + + - name: Check scalafix lints + if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' 'scalafixAll --check' + + - name: scalaJSLink + if: matrix.project == 'rootJS' + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' Test/scalaJSLinkerResult + + - name: Test + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' test + + - name: Check binary compatibility + if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' mimaReportBinaryIssues + + - name: Generate API documentation + if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' doc + + - name: Make target directories + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') + run: mkdir -p core/.js/target core/.jvm/target project/target + + - name: Compress target directories + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') + run: tar cf targets.tar core/.js/target core/.jvm/target project/target + + - name: Upload target directories + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') + uses: actions/upload-artifact@v5 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-${{ matrix.scala }}-${{ matrix.project }} + path: targets.tar + + publish: + name: Publish Artifacts + needs: [build] + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') + strategy: + matrix: + os: [ubuntu-22.04] + java: [temurin@8] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout current branch (full) + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup sbt + uses: sbt/setup-sbt@v1 + + - name: Setup Java (temurin@8) + id: setup-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 8 + cache: sbt + + - name: sbt update + if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' + run: sbt +update + + - name: Download target directories (2.13, rootJS) + uses: actions/download-artifact@v6 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootJS + + - name: Inflate target directories (2.13, rootJS) + run: | + tar xf targets.tar + rm targets.tar + + - name: Download target directories (2.13, rootJVM) + uses: actions/download-artifact@v6 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13-rootJVM + + - name: Inflate target directories (2.13, rootJVM) + run: | + tar xf targets.tar + rm targets.tar + + - name: Download target directories (3, rootJS) + uses: actions/download-artifact@v6 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootJS + + - name: Inflate target directories (3, rootJS) + run: | + tar xf targets.tar + rm targets.tar + + - name: Download target directories (3, rootJVM) + uses: actions/download-artifact@v6 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootJVM + + - name: Inflate target directories (3, rootJVM) + run: | + tar xf targets.tar + rm targets.tar + + - name: Import signing key + if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE == '' + env: + PGP_SECRET: ${{ secrets.PGP_SECRET }} + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + run: echo $PGP_SECRET | base64 -d -i - | gpg --import + + - name: Import signing key and strip passphrase + if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE != '' + env: + PGP_SECRET: ${{ secrets.PGP_SECRET }} + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + run: | + echo "$PGP_SECRET" | base64 -d -i - > /tmp/signing-key.gpg + echo "$PGP_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --import /tmp/signing-key.gpg + (echo "$PGP_PASSPHRASE"; echo; echo) | gpg --command-fd 0 --pinentry-mode loopback --change-passphrase $(gpg --list-secret-keys --with-colons 2> /dev/null | grep '^sec:' | cut --delimiter ':' --fields 5 | tail -n 1) + + - name: Publish + env: + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_CREDENTIAL_HOST: ${{ secrets.SONATYPE_CREDENTIAL_HOST }} + run: sbt tlCiRelease + + dependency-submission: + name: Submit Dependencies + if: github.event.repository.fork == false && github.event_name != 'pull_request' + strategy: + matrix: + os: [ubuntu-22.04] + java: [temurin@8] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout current branch (full) + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup sbt + uses: sbt/setup-sbt@v1 + + - name: Setup Java (temurin@8) + id: setup-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 8 + cache: sbt + + - name: sbt update + if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' + run: sbt +update + + - name: Submit Dependencies + uses: scalacenter/sbt-dependency-submission@v2 + with: + modules-ignore: rootjs_2.13 rootjs_3 docs_2.13 docs_3 rootjvm_2.13 rootjvm_3 rootnative_2.13 rootnative_3 + configs-ignore: test scala-tool scala-doc-tool test-internal + + site: + name: Generate Site + strategy: + matrix: + os: [ubuntu-22.04] + java: [temurin@11] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout current branch (full) + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup sbt + uses: sbt/setup-sbt@v1 + + - name: Setup Java (temurin@8) + id: setup-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 8 + cache: sbt + + - name: sbt update + if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' + run: sbt +update + + - name: Setup Java (temurin@11) + id: setup-java-temurin-11 + if: matrix.java == 'temurin@11' + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 11 + cache: sbt + + - name: sbt update + if: matrix.java == 'temurin@11' && steps.setup-java-temurin-11.outputs.cache-hit == 'false' + run: sbt +update + + - name: Generate site + run: sbt docs/tlSite + + - name: Publish site + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + uses: peaceiris/actions-gh-pages@v4.0.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: site/target/docs/site + keep_files: true diff --git a/.github/workflows/clean.yml b/.github/workflows/clean.yml new file mode 100644 index 0000000..547aaa4 --- /dev/null +++ b/.github/workflows/clean.yml @@ -0,0 +1,59 @@ +# This file was automatically generated by sbt-github-actions using the +# githubWorkflowGenerate task. You should add and commit this file to +# your git repository. It goes without saying that you shouldn't edit +# this file by hand! Instead, if you wish to make changes, you should +# change your sbt build configuration to revise the workflow description +# to meet your needs, then regenerate this file. + +name: Clean + +on: push + +jobs: + delete-artifacts: + name: Delete Artifacts + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Delete artifacts + run: | + # Customize those three lines with your repository and credentials: + REPO=${GITHUB_API_URL}/repos/${{ github.repository }} + + # A shortcut to call GitHub API. + ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } + + # A temporary file which receives HTTP response headers. + TMPFILE=/tmp/tmp.$$ + + # An associative array, key: artifact name, value: number of artifacts of that name. + declare -A ARTCOUNT + + # Process all artifacts on this repository, loop on returned "pages". + URL=$REPO/actions/artifacts + while [[ -n "$URL" ]]; do + + # Get current page, get response headers in a temporary file. + JSON=$(ghapi --dump-header $TMPFILE "$URL") + + # Get URL of next page. Will be empty if we are at the last page. + URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') + rm -f $TMPFILE + + # Number of artifacts on this page: + COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) + + # Loop on all artifacts on this page. + for ((i=0; $i < $COUNT; i++)); do + + # Get name of artifact and count instances of this name. + name=$(jq <<<$JSON -r ".artifacts[$i].name?") + ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) + + id=$(jq <<<$JSON -r ".artifacts[$i].id?") + size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) + printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size + ghapi -X DELETE $REPO/actions/artifacts/$id + done + done diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b314d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# sbt +target/ +project/plugins/project/ +boot/ +lib_managed/ +src_managed/ + +# vim +*.sw? + +# intellij +.idea/ + +# ignore [ce]tags files +tags + +# metals +.metals/ +.bsp/ +.bloop/ +metals.sbt +.vscode + +# npm +node_modules/ diff --git a/.scalafix.conf b/.scalafix.conf new file mode 100644 index 0000000..3bb29c8 --- /dev/null +++ b/.scalafix.conf @@ -0,0 +1,5 @@ +rules = [ + OrganizeImports +] + +OrganizeImports.removeUnused = false diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..dcf013c --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,8 @@ +version = 3.7.1 +runner.dialect = scala213source3 + +fileOverride { + "glob:**/scala-3/**" { + runner.dialect = scala3 + } +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..82c2cbd --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,14 @@ +# Code of Conduct + +Every member of our community has the right to have their identity respected. The Typelevel community is dedicated to providing a positive experience for everyone, regardless of age, gender identity and expression, sexual orientation, disability, neurodivergence, physical appearance, body size, ethnicity, nationality, race, or religion (or lack thereof), education, or socio-economic status. + +Everyone is expected to follow the [Typelevel Code of Conduct] when discussing the project on the available communication channels. + + +## Moderation + +If you have any questions, concerns, or moderation requests, please contact a member of the project. + +- [Amir Saeid](mailto:amir@codiff.com) + +[Typelevel Code of Conduct]: https://typelevel.org/code-of-conduct diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..2e3e5f6 --- /dev/null +++ b/NOTICE @@ -0,0 +1,3 @@ +fairstream +Copyright 2026 codiff +Licensed under Apache License 2.0 (see LICENSE) diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a31721 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +## fairstream + +[Simple fair and terminating backtracking Monad Transformer](https://okmij.org/ftp/Computation/monads.html#fair-bt-stream) diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..e92779f --- /dev/null +++ b/build.sbt @@ -0,0 +1,35 @@ +// https://typelevel.org/sbt-typelevel/faq.html#what-is-a-base-version-anyway +ThisBuild / tlBaseVersion := "0.0" // your current series x.y + +ThisBuild / organization := "com.codiff" +ThisBuild / organizationName := "codiff" +ThisBuild / startYear := Some(2026) +ThisBuild / licenses := Seq(License.Apache2) +ThisBuild / developers := List( + // your GitHub handle and name + tlGitHubDev("amir", "Amir Saeid") +) + +// publish website from this branch +ThisBuild / tlSitePublishBranch := Some("main") + +val Scala213 = "2.13.18" +ThisBuild / crossScalaVersions := Seq(Scala213, "3.3.7") +ThisBuild / scalaVersion := Scala213 // the default Scala + +lazy val root = tlCrossRootProject.aggregate(core) + +lazy val core = crossProject(JVMPlatform, JSPlatform) + .crossType(CrossType.Pure) + .in(file("core")) + .settings( + name := "fairstream", + libraryDependencies ++= Seq( + "org.typelevel" %%% "cats-core" % "2.13.0", + "org.typelevel" %%% "cats-effect" % "3.6.3", + "org.scalameta" %%% "munit" % "1.2.2" % Test, + "org.typelevel" %%% "munit-cats-effect" % "2.1.0" % Test + ) + ) + +lazy val docs = project.in(file("site")).enablePlugins(TypelevelSitePlugin) diff --git a/core/src/main/scala/com/codiff/fairstream/Fair.scala b/core/src/main/scala/com/codiff/fairstream/Fair.scala new file mode 100644 index 0000000..1436eba --- /dev/null +++ b/core/src/main/scala/com/codiff/fairstream/Fair.scala @@ -0,0 +1,60 @@ +package com.codiff.fairstream + +import cats.{Alternative, Monad, StackSafeMonad} + +sealed trait Fair[+A] + +object Fair { + case object Nil extends Fair[Nothing] + case class One[+A](a: A) extends Fair[A] + case class Choice[+A](a: A, rest: Fair[A]) extends Fair[A] + + class Incomplete[+A](expr: => Fair[A]) extends Fair[A] { + lazy val step: Fair[A] = expr + } + + object Incomplete { + def apply[A](expr: => Fair[A]): Incomplete[A] = new Incomplete(expr) + + def unapply[A](s: Incomplete[A]): Some[Fair[A]] = Some(s.step) + } + + def empty[A]: Fair[A] = Nil + + def unit[A](a: A): Fair[A] = One(a) + + def constant[A](a: A): Fair[A] = Choice(a, Incomplete(constant(a))) + + def guard(cond: Boolean): Fair[Unit] = if (cond) unit(()) else empty + + def mplus[A](left: Fair[A], right: => Fair[A]): Fair[A] = left match { + case Nil => Incomplete(right) + case One(a) => Choice(a, right) + case Choice(a, r) => Choice(a, mplus(right, r)) + case Incomplete(i) => + right match { + case Nil => Incomplete(i) + case One(b) => Choice(b, i) + case Choice(b, r2) => Choice(b, Incomplete(mplus(i, r2))) + case Incomplete(j) => Incomplete(mplus(i, j)) + } + } + + implicit val fairMonad + : Monad[Fair] with Alternative[Fair] with StackSafeMonad[Fair] = + new Monad[Fair] with Alternative[Fair] with StackSafeMonad[Fair] { + def empty[A]: Fair[A] = Fair.empty + + def pure[A](a: A): Fair[A] = Fair.unit(a) + + def flatMap[A, B](fa: Fair[A])(f: A => Fair[B]): Fair[B] = fa match { + case Nil => Nil + case One(a) => f(a) + case Choice(a, r) => combineK(f(a), Incomplete(flatMap(r)(f))) + case Incomplete(i) => Incomplete(flatMap(i)(f)) + } + + def combineK[A](x: Fair[A], y: Fair[A]): Fair[A] = mplus(x, y) + } + +} diff --git a/core/src/main/scala/com/codiff/fairstream/FairT.scala b/core/src/main/scala/com/codiff/fairstream/FairT.scala new file mode 100644 index 0000000..c652c0f --- /dev/null +++ b/core/src/main/scala/com/codiff/fairstream/FairT.scala @@ -0,0 +1,77 @@ +package com.codiff.fairstream + +import cats.{Alternative, Applicative, Monad} + +sealed trait FairE[M[_], A] + +object FairE { + final case class Nil[M[_], A]() extends FairE[M, A] + final case class One[M[_], A](a: A) extends FairE[M, A] + final case class Choice[M[_], A](a: A, rest: FairT[M, A]) extends FairE[M, A] + final case class Incomplete[M[_], A](rest: FairT[M, A]) extends FairE[M, A] +} + +final case class FairT[M[_], A](run: M[FairE[M, A]]) + +object FairT { + def empty[M[_], A](implicit M: Applicative[M]): FairT[M, A] = + FairT(M.pure[FairE[M, A]](FairE.Nil())) + + def unit[M[_], A](a: A)(implicit M: Applicative[M]): FairT[M, A] = + FairT(M.pure[FairE[M, A]](FairE.One(a))) + + def suspend[M[_], A](s: FairT[M, A])(implicit + M: Applicative[M] + ): FairT[M, A] = + FairT(M.pure[FairE[M, A]](FairE.Incomplete(s))) + + def mplus[M[_], A](left: FairT[M, A], right: => FairT[M, A])(implicit + M: Monad[M] + ): FairT[M, A] = { + type E = FairE[M, A] + FairT(M.flatMap[E, E](left.run) { + case FairE.Nil() => M.pure[E](FairE.Incomplete(right)) + case FairE.One(a) => M.pure[E](FairE.Choice(a, right)) + case FairE.Choice(a, r) => M.pure[E](FairE.Choice(a, mplus(right, r))) + case FairE.Incomplete(i) => + M.map[E, E](right.run) { + case FairE.Nil() => FairE.Incomplete(i) + case FairE.One(b) => FairE.Choice(b, i) + case FairE.Choice(b, r2) => FairE.Choice(b, mplus(i, r2)) + case FairE.Incomplete(j) => FairE.Incomplete(mplus(i, j)) + } + }) + } + + def flatMap[M[_], A, B]( + fa: FairT[M, A] + )(f: A => FairT[M, B])(implicit M: Monad[M]): FairT[M, B] = { + type EB = FairE[M, B] + FairT(M.flatMap[FairE[M, A], EB](fa.run) { + case FairE.Nil() => M.pure[EB](FairE.Nil()) + case FairE.One(a) => f(a).run + case FairE.Choice(a, r) => mplus(f(a), suspend(flatMap(r)(f))).run + case FairE.Incomplete(i) => M.pure[EB](FairE.Incomplete(flatMap(i)(f))) + }) + } + + implicit def fairTMonad[M[_]: Monad] + : Monad[FairT[M, *]] with Alternative[FairT[M, *]] = + new Monad[FairT[M, *]] with Alternative[FairT[M, *]] { + def empty[A]: FairT[M, A] = FairT.empty + + def pure[A](a: A): FairT[M, A] = FairT.unit(a) + + def flatMap[A, B](fa: FairT[M, A])(f: A => FairT[M, B]): FairT[M, B] = + FairT.flatMap(fa)(f) + + def tailRecM[A, B](a: A)(f: A => FairT[M, Either[A, B]]): FairT[M, B] = + flatMap(f(a)) { + case Left(next) => tailRecM(next)(f) + case Right(b) => FairT.unit(b) + } + + def combineK[A](x: FairT[M, A], y: FairT[M, A]): FairT[M, A] = mplus(x, y) + } + +} diff --git a/core/src/main/scala/com/codiff/fairstream/Main.scala b/core/src/main/scala/com/codiff/fairstream/Main.scala new file mode 100644 index 0000000..3c7c0b7 --- /dev/null +++ b/core/src/main/scala/com/codiff/fairstream/Main.scala @@ -0,0 +1,26 @@ +/* + * Copyright 2026 codiff + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.codiff.fairstream + +import cats.effect.IO +import cats.effect.IOApp + +object Main extends IOApp.Simple { + + def run: IO[Unit] = + IO.println("Hello sbt-typelevel!") +} diff --git a/core/src/test/scala/com/codiff/fairstream/MainSuite.scala b/core/src/test/scala/com/codiff/fairstream/MainSuite.scala new file mode 100644 index 0000000..869c583 --- /dev/null +++ b/core/src/test/scala/com/codiff/fairstream/MainSuite.scala @@ -0,0 +1,28 @@ +/* + * Copyright 2026 codiff + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.codiff.fairstream + +import munit.CatsEffectSuite + +class MainSuite extends CatsEffectSuite { + + test("Main should exit succesfully") { + val main = Main.run.attempt + assertIO(main, Right(())) + } + +} diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..3a31721 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,3 @@ +## fairstream + +[Simple fair and terminating backtracking Monad Transformer](https://okmij.org/ftp/Computation/monads.html#fair-bt-stream) diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..224de77 --- /dev/null +++ b/flake.nix @@ -0,0 +1,27 @@ +{ + inputs = { + typelevel-nix.url = "github:typelevel/typelevel-nix"; + nixpkgs.follows = "typelevel-nix/nixpkgs"; + flake-utils.follows = "typelevel-nix/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils, typelevel-nix }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ typelevel-nix.overlays.default ]; + }; + in + { + devShell = pkgs.devshell.mkShell { + imports = [ typelevel-nix.typelevelShell ]; + name = "fairstream-shell"; + typelevelShell = { + jdk.package = pkgs.jdk8; + nodejs.enable = true; + }; + }; + } + ); +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..4d6c567 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.12.2 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..d0a6df4 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,5 @@ +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.8.4") +addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.8.4") +addSbtPlugin("org.typelevel" % "sbt-typelevel-scalafix" % "0.8.4") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.20.2") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.8.2") -- cgit v1.2.3