Skip to content

Commit 9ba8e93

Browse files
committed
add init*With methods + Signal.Ref.use
1 parent bedf215 commit 9ba8e93

File tree

2 files changed

+227
-51
lines changed

2 files changed

+227
-51
lines changed

kyo-core/shared/src/main/scala/kyo/Signal.scala

+99-2
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,33 @@ object Signal:
154154
@implicitNotFound(missingCanEqual)
155155
canEqual: CanEqual[A, A]
156156
): Ref[A] < IO =
157-
IO.Unsafe(new Ref(Ref.Unsafe.init(initial)))
157+
initRefWith[A](initial)(identity)
158+
159+
/** Creates a new mutable signal reference with an initial value and applies a transformation function.
160+
*
161+
* This method initializes a new `Signal.Ref[A]` that can be modified over time, and immediately applies a transformation function to
162+
* it. The reference starts with the provided initial value and the transformation is applied within the same atomic operation.
163+
*
164+
* @param initial
165+
* The starting value for the signal reference
166+
* @param f
167+
* The transformation function to apply to the newly created reference
168+
* @return
169+
* The result of applying the transformation function
170+
* @tparam A
171+
* The type of value contained in the signal. Must have an instance of `CanEqual[A, A]`
172+
* @tparam B
173+
* The return type of the transformation function
174+
* @tparam S
175+
* The effect type of the transformation function
176+
*/
177+
def initRefWith[A](initial: A)[B, S](f: Ref[A] => B < S)(
178+
using
179+
frame: Frame,
180+
@implicitNotFound(missingCanEqual)
181+
canEqual: CanEqual[A, A]
182+
): B < (S & IO) =
183+
IO.Unsafe(f(new Ref(Ref.Unsafe.init(initial))))
158184

159185
/** Creates a new immutable signal with a constant value.
160186
*
@@ -179,6 +205,32 @@ object Signal:
179205
nextWith = [B, S] => f => f(value)
180206
)
181207

208+
/** Creates a new immutable signal with a constant value and applies a transformation function.
209+
*
210+
* This method creates a signal that always returns the same value and immediately applies a transformation function to it. Unlike
211+
* `Signal.Ref`, this signal cannot be modified after creation.
212+
*
213+
* @param value
214+
* The constant value for the signal
215+
* @param f
216+
* The transformation function to apply to the newly created signal
217+
* @return
218+
* The result of applying the transformation function
219+
* @tparam A
220+
* The type of value contained in the signal. Must have an instance of `CanEqual[A, A]`
221+
* @tparam B
222+
* The return type of the transformation function
223+
* @tparam S
224+
* The effect type of the transformation function
225+
*/
226+
def initConstWith[A](value: A)[B, S](f: Signal[A] => B < S)(
227+
using
228+
frame: Frame,
229+
@implicitNotFound(missingCanEqual)
230+
canEqual: CanEqual[A, A]
231+
): B < S =
232+
f(initConst(value))
233+
182234
/** Creates a new signal by specifying its fundamental operations.
183235
*
184236
* This is a lower-level constructor that allows direct implementation of a signal's behavior through its currentWith and nextWith
@@ -205,6 +257,39 @@ object Signal:
205257
): Signal[A] =
206258
_initRaw(currentWith, nextWith)
207259

260+
/** Creates a new signal by specifying its fundamental operations and applies a transformation function.
261+
*
262+
* This is a lower-level constructor that allows direct implementation of a signal's behavior through its currentWith and nextWith
263+
* operations, and immediately applies a transformation function to the created signal. It's primarily intended for implementing signal
264+
* combinators and custom signal types.
265+
*
266+
* @param currentWith
267+
* The implementation of currentWith, handling synchronous value access and transformation
268+
* @param nextWith
269+
* The implementation of nextWith, handling asynchronous value changes and transformation
270+
* @param f
271+
* The transformation function to apply to the newly created signal
272+
* @tparam A
273+
* The type of value contained in the signal. Must have an instance of `CanEqual[A, A]`
274+
* @tparam B
275+
* The return type of the transformation function
276+
* @tparam S
277+
* The effect type of the transformation function
278+
* @return
279+
* The result of applying the transformation function
280+
*/
281+
@nowarn("msg=anonymous")
282+
inline def initRawWith[A](
283+
inline currentWith: [B, S] => (A => B < S) => B < (S & IO),
284+
inline nextWith: [B, S] => (A => B < S) => B < (S & Async)
285+
)[B, S](f: Signal[A] => B < S)(
286+
using
287+
frame: Frame,
288+
@implicitNotFound(missingCanEqual)
289+
canEqual: CanEqual[A, A]
290+
): B < S =
291+
f(initRaw(currentWith, nextWith))
292+
208293
// Separated from initRaw to avoid name conflicts between parameters and Signal members
209294
@nowarn("msg=anonymous")
210295
private inline def _initRaw[A](
@@ -244,7 +329,19 @@ object Signal:
244329
* @return
245330
* The current value
246331
*/
247-
def get(using Frame): A < IO = IO.Unsafe(_unsafe.get())
332+
def get(using Frame): A < IO = use(identity)
333+
334+
/** Retrieves and transforms the current value of the reference.
335+
*
336+
* This is a convenience method that provides synchronous access to the reference's current value while applying a transformation
337+
* function. It's equivalent to `currentWith` but with a more familiar name for reference types.
338+
*
339+
* @param f
340+
* The transformation function to apply to the current value
341+
* @return
342+
* The transformed value wrapped in combined effects S & IO
343+
*/
344+
inline def use[B, S](inline f: A => B < S)(using Frame): B < (S & IO) = IO.Unsafe(f(_unsafe.get()))
248345

249346
/** Sets the reference to a new value.
250347
*

kyo-core/shared/src/test/scala/kyo/SignalTest.scala

+128-49
Original file line numberDiff line numberDiff line change
@@ -4,67 +4,137 @@ import kyo.debug.Debug
44

55
class SignalTest extends Test:
66

7-
"initRef" - {
8-
"ok" in run {
9-
for
10-
ref <- Signal.initRef(42)
11-
v <- ref.current
12-
yield assert(v == 42)
13-
}
14-
"missing CanEqual" in {
15-
typeCheckFailure(
16-
"Signal.initRef(Thread.currentThread())"
17-
)(
18-
"Cannot create Signal"
19-
)
7+
"init" - {
8+
"initRef" - {
9+
"ok" in run {
10+
for
11+
ref <- Signal.initRef(42)
12+
v <- ref.current
13+
yield assert(v == 42)
14+
}
15+
"missing CanEqual" in {
16+
typeCheckFailure(
17+
"Signal.initRef(Thread.currentThread())"
18+
)(
19+
"Cannot create Signal"
20+
)
21+
}
2022
}
21-
}
2223

23-
"initConst" - {
24-
"ok" in run {
25-
val sig = Signal.initConst(42)
26-
for
27-
v1 <- sig.current
28-
v2 <- sig.next
29-
yield assert(v1 == 42 && v2 == 42)
30-
end for
31-
}
32-
"missing CanEqual" in {
33-
typeCheckFailure(
34-
"Signal.initConst(Thread.currentThread())"
35-
)(
36-
"Cannot create Signal"
37-
)
24+
"initConst" - {
25+
"ok" in run {
26+
val sig = Signal.initConst(42)
27+
for
28+
v1 <- sig.current
29+
v2 <- sig.next
30+
yield assert(v1 == 42 && v2 == 42)
31+
end for
32+
}
33+
"missing CanEqual" in {
34+
typeCheckFailure(
35+
"Signal.initConst(Thread.currentThread())"
36+
)(
37+
"Cannot create Signal"
38+
)
39+
}
3840
}
39-
}
4041

41-
"initRaw" - {
42-
"ok" in run {
43-
val sig = Signal.initRaw[Int](
44-
currentWith = [B, S] => f => f(1),
45-
nextWith = [B, S] => f => f(2)
46-
)
47-
for
48-
v1 <- sig.current
49-
v2 <- sig.next
50-
yield assert(v1 == 1 && v2 == 2)
51-
end for
52-
}
53-
"missing CanEqual" in {
54-
typeCheckFailure(
55-
"""
42+
"initRaw" - {
43+
"ok" in run {
44+
val sig = Signal.initRaw[Int](
45+
currentWith = [B, S] => f => f(1),
46+
nextWith = [B, S] => f => f(2)
47+
)
48+
for
49+
v1 <- sig.current
50+
v2 <- sig.next
51+
yield assert(v1 == 1 && v2 == 2)
52+
end for
53+
}
54+
"missing CanEqual" in {
55+
typeCheckFailure(
56+
"""
5657
Signal.initRaw[Thread](
5758
currentWith = [B, S] => f => f(Thread.currentThread),
5859
nextWith = [B, S] => f => f(Thread.currentThread)
5960
)
6061
"""
61-
)(
62-
"Cannot create Signal"
63-
)
62+
)(
63+
"Cannot create Signal"
64+
)
65+
}
66+
}
67+
68+
"initRefWith" - {
69+
"ok" in run {
70+
for
71+
v <- Signal.initRefWith(42) { ref =>
72+
for
73+
_ <- ref.set(43)
74+
v <- ref.current
75+
yield v
76+
}
77+
yield assert(v == 43)
78+
}
79+
"missing CanEqual" in {
80+
typeCheckFailure(
81+
"Signal.initRefWith(Thread.currentThread())(identity)"
82+
)(
83+
"Cannot create Signal"
84+
)
85+
}
86+
}
87+
88+
"initConstWith" - {
89+
"ok" in run {
90+
for
91+
v <- Signal.initConstWith(42) { sig =>
92+
for
93+
v1 <- sig.current
94+
v2 <- sig.next
95+
yield (v1, v2)
96+
}
97+
yield assert(v == (42, 42))
98+
}
99+
"missing CanEqual" in {
100+
typeCheckFailure(
101+
"Signal.initConstWith(Thread.currentThread())(identity)"
102+
)(
103+
"Cannot create Signal"
104+
)
105+
}
106+
}
107+
108+
"initRawWith" - {
109+
"ok" in run {
110+
for
111+
v <- Signal.initRawWith[Int](
112+
currentWith = [B, S] => f => f(1),
113+
nextWith = [B, S] => f => f(2)
114+
) { sig =>
115+
for
116+
v1 <- sig.current
117+
v2 <- sig.next
118+
yield (v1, v2)
119+
}
120+
yield assert(v == (1, 2))
121+
}
122+
"missing CanEqual" in {
123+
typeCheckFailure(
124+
"""
125+
Signal.initRawWith[Thread](
126+
currentWith = [B, S] => f => f(Thread.currentThread),
127+
nextWith = [B, S] => f => f(Thread.currentThread)
128+
)(identity)
129+
"""
130+
)(
131+
"Cannot create Signal"
132+
)
133+
}
64134
}
65135
}
66136

67-
"Ref operations" - {
137+
"Signal.Ref" - {
68138
"get and set" in run {
69139
for
70140
ref <- Signal.initRef(1)
@@ -106,6 +176,15 @@ class SignalTest extends Test:
106176
v2 <- ref.get
107177
yield assert(v1 == 2 && v2 == 2)
108178
}
179+
180+
"use" in run {
181+
for
182+
ref <- Signal.initRef(1)
183+
v1 <- ref.use(_ * 2)
184+
_ <- ref.set(2)
185+
v2 <- ref.use(_ * 2)
186+
yield assert(v1 == 2 && v2 == 4)
187+
}
109188
}
110189

111190
"Signal operations" - {

0 commit comments

Comments
 (0)