Skip to content

Instantly share code, notes, and snippets.

@fteychene
Created April 28, 2020 13:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fteychene/8788d1845bfa136d6e15cb3f1aae7497 to your computer and use it in GitHub Desktop.
Save fteychene/8788d1845bfa136d6e15cb3f1aae7497 to your computer and use it in GitHub Desktop.
Basic Zipper in Kotlin with Arrow
plugins {
kotlin("jvm") version "1.3.72"
kotlin("kapt") version "1.3.41"
}
group = "xyz.fteychene.kotlin.sample"
version = "0.1.0-SNAPSHOT"
val arrowVersion = "0.10.4"
val junitVersion = "5.6.2"
val kotlintestVersion = "4.0.5"
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation("io.arrow-kt:arrow-core:$arrowVersion")
implementation("io.arrow-kt:arrow-optics:$arrowVersion")
implementation("io.arrow-kt:arrow-syntax:$arrowVersion")
kapt("io.arrow-kt:arrow-meta:$arrowVersion")
testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion")
testImplementation("io.kotest:kotest-runner-junit5-jvm:$kotlintestVersion") // for kotest framework
testImplementation("io.kotest:kotest-assertions-core-jvm:$kotlintestVersion") // for kotest core jvm assertions
testImplementation("io.kotest:kotest-property:$kotlintestVersion") // for kotest property test
testImplementation("io.kotest:kotest-assertions-arrow-jvm:$kotlintestVersion") // for kotest core arrow assertions
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks {
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
}
import arrow.core.Option
import arrow.core.extensions.list.foldable.firstOption
import arrow.core.extensions.sequence.foldable.firstOption
import arrow.core.getOrElse
fun <A> Sequence<A>.headAndTail(): Pair<Option<A>, Sequence<A>> = firstOption() to drop(1)
fun <A> List<A>.headAndTail(): Pair<Option<A>, List<A>> = firstOption() to drop(1)
data class Zipper<A>(
val left: Sequence<A>,
val focus: A,
val right: Sequence<A>
) {
fun right(): Option<Zipper<A>> = right.headAndTail().let { (head, tail) ->
head.map { Zipper(sequenceOf(focus) + left, it, tail) }
}
fun left(): Option<Zipper<A>> = left.headAndTail().let { (head, tail) ->
head.map { Zipper(tail, it, sequenceOf(focus) + right) }
}
fun moveRight(): Zipper<A> = right().getOrElse { this }
fun moveLeft(): Zipper<A> = left().getOrElse { this }
fun asSequence(): Sequence<A> = left.toList().reversed().asSequence() + sequenceOf(focus) + right
fun asList(): List<A> = left.toList().reversed() + listOf(focus) + right.toList()
companion object {
fun <A> from(list: List<A>) : Option<Zipper<A>> =
list.headAndTail().let { (head, tail) ->
head.map { Zipper(emptySequence(), it, tail.asSequence()) }
}
}
}
package hexafp.basic.misc
import io.kotest.assertions.arrow.option.shouldBeNone
import io.kotest.assertions.arrow.option.shouldBeSome
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.collections.shouldContainInOrder
import io.kotest.matchers.shouldBe
class ZipperTest : StringSpec({
"headAndTail should return head and tail of stream" {
sequenceOf(1, 2, 3, 4, 5).headAndTail().let { (head, tail) ->
head shouldBeSome 1
tail.toList() shouldContainInOrder listOf(2, 3, 4, 5)
}
emptySequence<Int>().headAndTail().let { (head, tail) ->
head.shouldBeNone()
tail shouldBe emptySequence()
}
sequenceOf(1).headAndTail().let { (head, tail) ->
head shouldBeSome 1
tail.toList().shouldBeEmpty()
}
}
"right should return a new focused zipper if values exists on right" {
val base = Zipper(
left = emptySequence(),
focus = 1,
right = sequenceOf(2, 3, 4, 5)
)
// Assert new value
base.right() shouldBeSome {
it.focus shouldBe 2
it.left.toList() shouldContainExactly listOf(1)
it.right.toList() shouldContainExactly listOf(3, 4, 5)
}
// Assert base didn't changed
base.focus shouldBe 1
base.left.toList().shouldBeEmpty()
base.right.toList() shouldBe listOf(2, 3, 4, 5)
}
"right should return a none if no value on right" {
val base = Zipper(
left = sequenceOf(2, 3, 4, 5),
focus = 1,
right = emptySequence()
)
// Assert new value
base.right().shouldBeNone()
// Assert base didn't changed
base.focus shouldBe 1
base.left.toList() shouldBe listOf(2, 3, 4, 5)
base.right.toList().shouldBeEmpty()
}
"right should concatenate focus on the head of left" {
val base = Zipper(
left = sequenceOf(3, 4, 5),
focus = 2,
right = sequenceOf(1)
)
// Assert new value
base.right() shouldBeSome {
it.focus shouldBe 1
it.left.toList() shouldContainExactly listOf(2, 3, 4, 5)
it.right.toList().shouldBeEmpty()
}
// Assert base didn't changed
base.focus shouldBe 2
base.left.toList() shouldBe listOf(3, 4, 5)
base.right.toList() shouldBe listOf(1)
}
"left should return a new focused zipper if values exists on left" {
val base = Zipper(
left = sequenceOf(2, 3, 4, 5),
focus = 1,
right = emptySequence()
)
val actual = base.left()
// Assert new value
actual shouldBeSome {
it.focus shouldBe 2
it.left.toList() shouldContainExactly listOf(3, 4, 5)
it.right.toList() shouldContainExactly listOf(1)
}
// Assert base didn't changed
base.focus shouldBe 1
base.left.toList() shouldBe listOf(2, 3, 4, 5)
base.right.toList().shouldBeEmpty()
}
"left should return a none if no value on left" {
val base = Zipper(
left = emptySequence(),
focus = 1,
right = sequenceOf(2, 3, 4, 5)
)
val actual = base.left()
// Assert new value
actual.shouldBeNone()
// Assert base didn't changed
base.focus shouldBe 1
base.left.toList().shouldBeEmpty()
base.right.toList() shouldBe listOf(2, 3, 4, 5)
}
"left should concatenate focus on the head of right" {
val base = Zipper(
left = sequenceOf(3, 4, 5),
focus = 2,
right = sequenceOf(1)
)
// Assert new value
base.left() shouldBeSome {
it.focus shouldBe 3
it.left.toList() shouldContainExactly listOf(4, 5)
it.right.toList() shouldContainExactly listOf(2, 1)
}
// Assert base didn't changed
base.focus shouldBe 2
base.left.toList() shouldBe listOf(3, 4, 5)
base.right.toList() shouldBe listOf(1)
}
"move right should return same zipper in no value on right" {
val base = Zipper(
left = sequenceOf(2, 3, 4, 5),
focus = 1,
right = emptySequence()
)
base.moveRight() shouldBe base
}
"move left should return same zipper in no value on left" {
val base = Zipper(
left = emptySequence(),
focus = 1,
right = sequenceOf(2, 3, 4, 5)
)
base.moveLeft() shouldBe base
}
"asSequence should generate a Sequence from values" {
Zipper(sequenceOf(2, 1), 3, sequenceOf(4, 5))
.asSequence().toList() shouldContainInOrder listOf(1, 2, 3, 4, 5)
Zipper(emptySequence(), 1, sequenceOf(2, 3, 4, 5))
.asSequence().toList() shouldContainInOrder listOf(1, 2, 3, 4, 5)
Zipper(sequenceOf(4, 3, 2, 1), 5, emptySequence())
.asSequence().toList() shouldContainInOrder listOf(1, 2, 3, 4, 5)
}
"asList should generate a List from values" {
Zipper(sequenceOf(2, 1), 3, sequenceOf(4, 5))
.asList() shouldContainInOrder listOf(1, 2, 3, 4, 5)
Zipper(emptySequence(), 1, sequenceOf(2, 3, 4, 5))
.asList() shouldContainInOrder listOf(1, 2, 3, 4, 5)
Zipper(sequenceOf(4, 3, 2, 1), 5, emptySequence())
.asList() shouldContainInOrder listOf(1, 2, 3, 4, 5)
}
"from a List create a Zipper with tail on the right" {
Zipper.from(listOf(1, 2, 3, 4)) shouldBeSome {
it.left shouldBe emptySequence()
it.focus shouldBe 1
it.right.toList() shouldContainInOrder listOf(2, 3, 4)
}
Zipper.from(listOf("1", "2", "3", "4")) shouldBeSome {
it.left shouldBe emptySequence()
it.focus shouldBe "1"
it.right.toList() shouldContainInOrder listOf("2", "3", "4")
}
Zipper.from(listOf(1)) shouldBeSome {
it.left shouldBe emptySequence()
it.focus shouldBe 1
it.right.toList().shouldBeEmpty()
}
}
"from a List should not create Zipper on empty List" {
Zipper.from(emptyList<Int>()).shouldBeNone()
}
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment