Created
April 28, 2020 13:57
-
-
Save fteychene/8788d1845bfa136d6e15cb3f1aae7497 to your computer and use it in GitHub Desktop.
Basic Zipper in Kotlin with Arrow
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) } | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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