Skip to content

Commit

Permalink
Add immutable ordered finite 'map' and 'set' to the stdlib (#561)
Browse files Browse the repository at this point in the history
I've been sitting on this code for almost ~~9~~ _13_ months now (started
on Xmas 2023), it's about time we finalise it.
This PR adds immutable ordered maps and sets based on weight balanced
trees into the standard library.

The `map` and `set` now work on all main backends: JS, LLVM, Chez.

Progress ~~is **blocked**~~ was blocked on:
1. ~~very annoying inliner bug(s?) [#733]~~ _resolved_ 🥳 
2. ~~missing comparisons on strings on LLVM [#748]~~ _resolved_ 🎉
  • Loading branch information
jiribenes authored Jan 23, 2025
1 parent e4be2f9 commit 2be5198
Show file tree
Hide file tree
Showing 19 changed files with 1,640 additions and 11 deletions.
7 changes: 7 additions & 0 deletions examples/stdlib/map/cache.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Get key1: value1
Get key2: value2
Get key1: newValue1
Get key2: value2
After cleaning:
Get key1: newValue1
Get key2: Not found
50 changes: 50 additions & 0 deletions examples/stdlib/map/cache.effekt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import map
import bytearray

record CacheEntry[V](value: V, timestamp: Int)

effect time(): Int

def main() = {
var currentTime = 0
try {
var cache: Map[String, CacheEntry[String]] = map::empty(compareStringBytes)
val maxAge = 8

def cachePut(key: String, value: String): Unit =
cache = cache.put(key, CacheEntry(value, do time()))

def cacheGet(key: String): Option[String] =
cache.get(key) match {
case Some(entry) and do time() - entry.timestamp < maxAge => Some(entry.value)
case _ => None()
}

def cleanExpiredEntries(): Unit = {
val currentTime = do time()
cache = cache.filter { (_, entry) =>
currentTime - entry.timestamp < maxAge
}
}

cachePut("key1", "value1")
cachePut("key2", "value2")

println("Get key1: " ++ cacheGet("key1").getOrElse { "Not found" })
println("Get key2: " ++ cacheGet("key2").getOrElse { "Not found" })

cachePut("key1", "newValue1")

println("Get key1: " ++ cacheGet("key1").getOrElse { "Not found" })
println("Get key2: " ++ cacheGet("key2").getOrElse { "Not found" })

cleanExpiredEntries()

println("After cleaning:")
println("Get key1: " ++ cacheGet("key1").getOrElse { "Not found" })
println("Get key2: " ++ cacheGet("key2").getOrElse { "Not found" })
} with time {
currentTime = currentTime + 1
resume(currentTime)
}
}
7 changes: 7 additions & 0 deletions examples/stdlib/map/counter.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ask: 3
can: 3
you: 3
see: 0
do: 4
Effekt: 2
[Effekt → 2, and → 1, ask → 3, but → 1, can → 3, do → 4, fellow → 2, for → 4, language → 2, man → 1, my → 2, not → 2, of → 2, programmers → 2, programs → 1, so → 1, the → 2, together → 1, we → 1, what → 4, will → 1, world → 1, you → 3, your → 2]
40 changes: 40 additions & 0 deletions examples/stdlib/map/counter.effekt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import map
import bytearray

def counter(words: List[String]): Map[String, Int] = {
var m: Map[String, Int] = map::empty(compareStringBytes);

list::foreach(words) { word =>
m = m.putWithKey(word, 1) { (_, n1, n2) => n1 + n2 }
}

m
}

def main() = {
// John F. Kennedy's Inaugural Address, Jan 20, 1961; modified for Effekt
val speech: List[String] = [
"and", "so", "my", "fellow", "Effekt", "programmers",
"ask", "not", "what", "your", "language", "can", "do", "for", "you",
"ask", "what", "you", "can", "do", "for", "your", "language",
"my", "fellow", "programmers", "of", "the", "world",
"ask", "not", "what", "Effekt", "will", "do", "for", "you",
"but", "what", "together", "we", "can", "do", "for", "the", "programs", "of", "man"
]

val ctr: Map[String, Int] = counter(speech)

def test(word: String) = {
val count = ctr.getOrElse(word) { 0 }
println(word ++ ": " ++ count.show)
}

test("ask")
test("can")
test("you")
test("see")
test("do")
test("Effekt")

println(map::internal::prettyPairs(ctr.toList) { s => show(s) } { n => show(n) })
}
7 changes: 7 additions & 0 deletions examples/stdlib/map/map.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[0 → Hello, 1 → World, 2 → Woo!]
[-1 → Hullo, 0 → Hello, 1 → World, 2 → Woo!]
[-10 → EY, -1 → Hullo, 0 → Hello, 1 → World, 2 → Woo!]
[0 → Hello, 2 → Woo!, 42 → Whole new world!, 100 → Big, 1000 → Bigger, 10000 → Biggest!]
[-1 → Huh?!, 0 → Hello, 1 → Foo, 2 → Woo!, 42 → Whole new world!, 100 → Big, 1000 → Bigger, 10000 → Biggest!]
[-1 → Huh?!, 0 → Hello, 1 → Foo, 2 → Woo!, 42 → Whole new world!, 100 → Big, 1000 → Bigger, 10000 → Biggest!]
[0 → Hello, 1 → World, 2 → Woo!]
28 changes: 28 additions & 0 deletions examples/stdlib/map/map.effekt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import map

def main() = {
val l = [(0, "Hello"), (1, "World"), (2, "Woo!")]

val m = map::fromList(l, compareInt)
println(map::internal::prettyPairs(m.toList) { n => show(n) } { s => show(s) })

val m2 = m.put(-1, "Hullo")
println(map::internal::prettyPairs(m2.toList) { n => show(n) } { s => show(s) })

val m3 = m2.put(-10, "EY")
println(map::internal::prettyPairs(m3.toList) { n => show(n) } { s => show(s) })

// ...

val m4 = m.delete(1).put(42, "Whole new world!").put(100, "Big").put(1000, "Bigger").put(10000, "Biggest!")
println(map::internal::prettyPairs(m4.toList) { n => show(n) } { s => show(s) })

val m5 = map::fromList(Cons((1, "Foo"), Cons((-1, "Huh?!"), m4.toList.reverse)), compareInt)
println(map::internal::prettyPairs(m5.toList) { n => show(n) } { s => show(s) })

val m6: Map[Int, String] = m5.toList.fromList(compareInt)
println(map::internal::prettyPairs(m6.toList) { n => show(n) } { s => show(s) })

val nuMap = map::fromList(l.reverse, compareInt)
println(map::internal::prettyPairs(nuMap.toList) { n => show(n) } { s => show(s) })
}
2 changes: 2 additions & 0 deletions examples/stdlib/map/minmax.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Min: 5 -> Five
Max: 30 -> Thirty
16 changes: 16 additions & 0 deletions examples/stdlib/map/minmax.effekt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import map

def show(opt: Option[(Int, String)]): String = opt match {
case Some((k, v)) => k.show ++ " -> " ++ v
case None() => "X"
}

def main() = {
val m = map::fromList([(10, "Ten"), (20, "Twenty"), (5, "Five"), (30, "Thirty")], compareInt)

val min: Option[(Int, String)] = m.getMin
val max: Option[(Int, String)] = m.getMax

println("Min: " ++ min.show)
println("Max: " ++ max.show)
}
8 changes: 8 additions & 0 deletions examples/stdlib/map/setops.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Union (keeping first value):
[1 → apple, 2 → banana, 3 → cherry, 4 → elderberry]

Union (combining values):
[1 → apple, 2 → banana/berry, 3 → cherry/date, 4 → elderberry]

Intersection (combining keys):
[2 → banana-berry, 3 → cherry-date]
20 changes: 20 additions & 0 deletions examples/stdlib/map/setops.effekt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import map

def main() = {
// Create multiple maps
val map1 = map::fromList([(1, "apple"), (2, "banana"), (3, "cherry")], compareInt)
val map2 = map::fromList([(2, "berry"), (3, "date"), (4, "elderberry")], compareInt)

// Test union with different combine strategies
println("Union (keeping first value):")
println(map::internal::prettyPairs(map1.union(map2).toList) { n => show(n) } { s => show(s) })

println("\nUnion (combining values):")
val combinedMap = map1.union(map2, compareInt) { (k, v1, v2) => v1 ++ "/" ++ v2 }
println(map::internal::prettyPairs(combinedMap.toList) { n => show(n) } { s => show(s) })

// Test intersection
println("\nIntersection (combining keys):")
val intersectedMap = map1.intersection(map2, compareInt) { (k, v1, v2) => v1 ++ "-" ++ v2 }
println(map::internal::prettyPairs(intersectedMap.toList) { n => show(n) } { s => show(s) })
}
23 changes: 23 additions & 0 deletions examples/stdlib/set/unique.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
ask: true
can: true
you: true
see: false
do: true
Effekt: true

Cons(Effekt, Cons(and, Cons(ask, Cons(but, Cons(can, Cons(do, Cons(fellow, Cons(for, Cons(language, Cons(man, Cons(my, Cons(not, Cons(of, Cons(programmers, Cons(programs, Cons(so, Cons(the, Cons(together, Cons(we, Cons(what, Cons(will, Cons(world, Cons(you, Cons(your, Nil()))))))))))))))))))))))))
sorted
Cons(Effekt, Cons(and, Cons(ask, Cons(but, Cons(can, Cons(do, Cons(fellow, Cons(for, Cons(language, Cons(man, Cons(my, Cons(not, Cons(of, Cons(programmers, Cons(programs, Cons(so, Cons(the, Cons(together, Cons(we, Cons(what, Cons(will, Cons(world, Cons(you, Cons(your, Nil()))))))))))))))))))))))))
sorted

Cons(after, Cons(around, Cons(better, Cons(do, Cons(ever, Cons(faster, Cons(harder, Cons(hour, Cons(is, Cons(it, Cons(make, Cons(makes, Cons(more, Cons(never, Cons(over, Cons(stronger, Cons(than, Cons(the, Cons(us, Cons(work, Cons(world, Nil())))))))))))))))))))))
sorted

lyrics / speech:
Cons(after, Cons(around, Cons(better, Cons(ever, Cons(faster, Cons(harder, Cons(hour, Cons(is, Cons(it, Cons(make, Cons(makes, Cons(more, Cons(never, Cons(over, Cons(stronger, Cons(than, Cons(us, Cons(work, Nil()))))))))))))))))))
speech / lyrics:
Cons(Effekt, Cons(and, Cons(ask, Cons(but, Cons(can, Cons(fellow, Cons(for, Cons(language, Cons(man, Cons(my, Cons(not, Cons(of, Cons(programmers, Cons(programs, Cons(so, Cons(together, Cons(we, Cons(what, Cons(will, Cons(you, Cons(your, Nil())))))))))))))))))))))
speech n lyrics:
Cons(do, Cons(the, Cons(world, Nil())))
speech u lyrics:
Cons(Effekt, Cons(after, Cons(and, Cons(around, Cons(ask, Cons(better, Cons(but, Cons(can, Cons(do, Cons(ever, Cons(faster, Cons(fellow, Cons(for, Cons(harder, Cons(hour, Cons(is, Cons(it, Cons(language, Cons(make, Cons(makes, Cons(man, Cons(more, Cons(my, Cons(never, Cons(not, Cons(of, Cons(over, Cons(programmers, Cons(programs, Cons(so, Cons(stronger, Cons(than, Cons(the, Cons(together, Cons(us, Cons(we, Cons(what, Cons(will, Cons(work, Cons(world, Cons(you, Cons(your, Nil()))))))))))))))))))))))))))))))))))))))))))
110 changes: 110 additions & 0 deletions examples/stdlib/set/unique.effekt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import set
import bytearray

def unique(words: List[String]): Set[String] = {
var s: Set[String] = set::empty(compareStringBytes);

list::foreach(words) { word =>
s = s.insert(word)
}

s
}

def main() = {
// John F. Kennedy's Inaugural Address Jan 20 1961; modified for Effekt
val speech: List[String] = [
"and", "so", "my", "fellow", "Effekt", "programmers",
"ask", "not", "what", "your", "language", "can", "do", "for", "you",
"ask", "what", "you", "can", "do", "for", "your", "language",
"my", "fellow", "programmers", "of", "the", "world",
"ask", "not", "what", "Effekt", "will", "do", "for", "you",
"but", "what", "together", "we", "can", "do", "for", "the", "programs", "of", "man"
]

val uniqueSpeech: Set[String] = unique(speech)

def test(word: String) = {
val present = uniqueSpeech.contains(word)
println(word ++ ": " ++ present.show)
}

test("ask")
test("can")
test("you")
test("see")
test("do")
test("Effekt")

// ---
println("")

def testSorted(s: Set[String]) = {
val sorted = s.toList.isSortedBy { (x, y) =>
x.compareStringBytes(y) match {
case Equal() => true
case Less() => true
case Greater() => false
}
}
if (sorted) {
println("sorted")
} else {
println("unsorted")
}
}

println(uniqueSpeech.toList)
testSorted(uniqueSpeech)

println(set::fromList(speech, compareStringBytes).toList)
testSorted(set::fromList(speech, compareStringBytes))

// ---
println("")

// Around the World / Harder, Better, Faster, Stronger by Daft Punk (Alive 2007)
val lyrics: List[String] = [
"around", "the", "world", "around", "the", "world",
"around", "the", "world", "around", "the", "world",
"around", "the", "world", "around", "the", "world",
"around", "the", "world", "around", "the", "world",
"around", "the", "world", "around", "the", "world",
"around", "the", "world", "around", "the", "world",
"around", "the", "world", "around", "the", "world",
"around", "the", "world", "around", "the", "world",

"work", "it", "make", "it", "do", "it", "makes", "us",
"harder", "better", "faster", "stronger",
"more", "than", "hour", "hour", "never",
"ever", "after", "work", "is", "over",

"work", "it", "make", "it", "do", "it", "makes", "us",
"around", "the", "world", "around", "the", "world",
"around", "the", "world", "around", "the", "world",

"harder", "better", "faster", "stronger",
"around", "the", "world", "around", "the", "world",
"around", "the", "world", "around", "the", "world"
]

val uniqueLyrics = unique(lyrics)

println(uniqueLyrics.toList)
testSorted(uniqueLyrics)

// ---
println("")

println("lyrics / speech:")
println(uniqueLyrics.difference(uniqueSpeech).toList)

println("speech / lyrics:")
println(uniqueSpeech.difference(uniqueLyrics).toList)

println("speech n lyrics:")
println(uniqueLyrics.intersection(uniqueSpeech).toList)

println("speech u lyrics:")
println(uniqueLyrics.union(uniqueSpeech).toList)
}
7 changes: 7 additions & 0 deletions examples/stdlib/stream/map.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
1: H (72)
2: e (101)
3: l (108)
4: l (108)
5: o (111)
[1 → H, 2 → e, 3 → l, 4 → l, 5 → o]
Hello
17 changes: 17 additions & 0 deletions examples/stdlib/stream/map.effekt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import stream
import map

def main() = {
val m = map::fromList([(1, 'H'), (2, 'e'), (3, 'l'), (4, 'l'), (5, 'o')], compareInt)

for[(Int, Char)] { each(m) } { case (k, v) =>
println(show(k) ++ ": " ++ show(v) ++ " (" ++ show(v.toInt) ++ ")")
}

val newMap = collectMap[Int, Char](compareInt) { each(m) }
println(map::internal::prettyPairs(newMap.toList) { n => show(n) } { c => show(c) })

val hello: String = collectString { eachValue(m) }
println(hello)
}

21 changes: 15 additions & 6 deletions libraries/common/bytearray.effekt
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,19 @@ extern pure def compareByteArrayImpl(b1: ByteArray, b2: ByteArray): Int =
"""
chez "(bytearray$compare ${b1} ${b2})"

def compareByteArray(b1: ByteArray, b2: ByteArray): Ordering =
compareByteArrayImpl(b1, b2) match {
case -1 => Less()
case 0 => Equal()
case 1 => Greater()
case _ => <{ "Impossible: ByteArray comparison returned invalid value!" }>
def compareByteArray(b1: ByteArray, b2: ByteArray): Ordering = {
val ret = compareByteArrayImpl(b1, b2)
if (ret == 0) {
Equal()
} else if (ret < 0) {
Less()
} else { // ret > 0
Greater()
}
}

def compareStringBytes(left: String, right: String): Ordering = {
val l = left.fromString
val r = right.fromString
compareByteArray(l, r)
}
Loading

0 comments on commit 2be5198

Please sign in to comment.