Skip to content

Commit

Permalink
Towards a safe API for Exchanger
Browse files Browse the repository at this point in the history
  • Loading branch information
durban committed Dec 9, 2024
1 parent 929a669 commit 67ddff0
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 1 deletion.
58 changes: 58 additions & 0 deletions core/shared/src/main/scala/dev/tauri/choam/core/Eliminator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2016-2024 Daniel Urban and contributors listed in NOTICE.txt
*
* 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 dev.tauri.choam
package core

sealed trait Eliminator[-A, +B, -C, +D] {
def leftOp: A =#> B
def rightOp: C =#> D
}

object Eliminator {

// TODO: on JS, the whole thing should be a no-op

def apply[A, B, C, D](
left: A =#> B,
tLeft: A => D,
right: C =#> D,
tRight: C => B,
): Axn[Eliminator[A, B, C, D]] = {
Axn.unsafe.delay {
new EliminatorImpl[A, B, C, D](left, tLeft, right, tRight) {}
}
}
}

/** Internally, we can use this by subclassing (to avoid an extra object) */
private[choam] abstract class EliminatorImpl[-A, +B, -C, +D] private[choam] (
underlyingLeft: A =#> B,
transformLeft: A => D,
underlyingRight: C =#> D,
transformRight: C => B,
) extends Eliminator[A, B, C, D] {

private[this] val exchanger: Exchanger[A, C] =
Exchanger.unsafe[A, C]

final override val leftOp: A =#> B =
underlyingLeft + exchanger.exchange.map(transformRight)

final override val rightOp: C =#> D =
underlyingRight + exchanger.dual.exchange.map(transformLeft)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2016-2024 Daniel Urban and contributors listed in NOTICE.txt
*
* 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 dev.tauri.choam
package data

import cats.effect.IO

final class EliminatorSpecJvm_Emcas_ZIO
extends BaseSpecZIO
with SpecEmcas
with EliminatorSpecJvm[zio.Task]

final class EliminatorSpecJvm_Emcas_IO
extends BaseSpecIO
with SpecEmcas
with EliminatorSpecJvm[IO]

trait EliminatorSpecJvm[F[_]] extends EliminatorSpec[F] { this: McasImplSpec =>

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ private final class TreiberStack[A] private () extends Stack[A] {
case End => (End, None)
}

final override val size: Axn[Int] =
final override val size: Axn[Int] = // TODO: this is O(n)
head.get.map(_.length)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2016-2024 Daniel Urban and contributors listed in NOTICE.txt
*
* 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 dev.tauri.choam
package data

import cats.effect.IO

import core.Eliminator

final class EliminatorSpec_ThreadConfinedMcas_IO
extends BaseSpecIO
with SpecThreadConfinedMcas
with EliminatorSpec[IO]

trait EliminatorSpec[F[_]] extends BaseSpecAsyncF[F] { this: McasImplSpec =>

final class EliminationStackForTesting[A] private (
underlying: TreiberStack[A],
eliminator: Eliminator[A, Unit, Any, Option[A]],
) extends Stack[A] {

override def push: Rxn[A, Unit] =
eliminator.leftOp

override def tryPop: Axn[Option[A]] =
eliminator.rightOp

override def size: Axn[Int] =
underlying.size
}

final object EliminationStackForTesting {
def apply[A]: Axn[EliminationStackForTesting[A]] = {
TreiberStack[A].flatMapF { underlying =>
Eliminator[A, Unit, Any, Option[A]](
underlying.push,
Some(_),
underlying.tryPop,
_ => (),
).map { eliminator =>
new EliminationStackForTesting(underlying, eliminator)
}
}
}
}

test("Eliminator.apply") {
for {
e <- Eliminator[String, String, String, String](
Rxn.lift(s => s + "x"),
s => s,
Rxn.lift(s => s + "y"),
s => s,
).run[F]
_ <- assertResultF(e.leftOp[F]("a"), "ax")
_ <- assertResultF(e.rightOp[F]("b"), "by")
} yield ()
}

test("EliminationStackForTesting (basic)") {
for {
s <- EliminationStackForTesting[Int].run[F]
_ <- assertResultF(s.tryPop.run[F], None)
_ <- s.push[F](1)
_ <- (s.push.provide(2) *> s.push.provide(3)).run[F]
_ <- assertResultF(s.tryPop.run[F], Some(3))
_ <- assertResultF((s.tryPop * s.tryPop).run[F], (Some(2), Some(1)))
_ <- assertResultF(s.tryPop.run[F], None)
} yield ()
}
}

0 comments on commit 67ddff0

Please sign in to comment.