Hey, I've recently been exploring the effect system as a way to stop people from calling coroutine-only functions outside of a coroutine. This seems like a prime use-case for effects.
# coro.nim
type YieldEffect = ref object of RootEffect
proc jield*() {.tags: [YieldEffect].} =
discard # todo: implement me
In theory, I could annotate my application's main() proc with {.tags: [].} which would guarantee jield() is never called on the main code path. This unfortunately falls apart because callbacks and forward-declared procs are assumed to have any and all effects, unless their effects are explicitly annotated.
# foo.nim
var callback: proc()
proc setCallback*(f: proc()) {.effectsOf: [].} =
callback = f
proc doCallback*() =
callback()
# main.nim
import foo
proc main() {.tags: [].} =
setCallback(proc () =
echo "hello world!"
)
doCallback() # Error: doCallback() can have an unlisted effect: RootEffect
main()
Library authors aren't gonna put {.tags: [].} on their callbacks (nor should they, as they can't assume that all effects are bad), therefore most libraries will accidentally bestow RootEffect on you at some point.
I can get closer to a workable solution by using a non-inherited type. This way I can permit RootEffect and anything that inherits from it, but anything else is still an error.
# coro.nim
type YieldEffect = object
# ...
# main.nim
import foo
proc main() {.tags: [RootEffect].} =
setCallback(proc () =
echo "hello world!"
)
doCallback() # ok!
# jield() # correctly prevented :)
main()
But now the opposite problem occurs, it's easy for the effect to be "lost", for example by assigning to a proc variable, or calling a foward-declared proc, or using effectsOf from Nim 1.6:
# main.nim
import foo
proc bar()
proc main() {.tags: [RootEffect].} =
setCallback(proc () =
echo "hello world!"
jield()
)
var f: proc()
f = proc() =
echo "hello world!"
jield()
doCallback() # allowed :(
f() # allowed :(
bar() # allowed :(
main()
proc bar() =
jield()
I believe what I'm really looking for is a "forbidden" or "must be explicit" effect which is always an error when assigned-to, passed-to or called-within any proc whose effects are left unspecified. The effect would only be allowed if it appears in the tag list.
The onus is then on the propagators of the effect to always list it, rather than every possible reciever of the effect to reject it.
Maybe it would look something like this?
type
YieldEffect = ref object of ExplicitEffect
YieldEffect {.forbidden.} = object # or like this...
YieldEffect {.explicit.} = object # or like this...
YieldEffect = distinct object # or like this?
Does this sound like a good idea or am I overlooking something?
Does this sound like a good idea or am I overlooking something?
It does sound like a good idea, assuming that the effect system is a good idea. ;-)
How about:
type YieldPermission = object
proc `=copy`(dest: var YieldEffect, src: YieldEffect) {.error.}
# you cannot duplicate yield permission
proc jield*(y: YieldPermission) =
discard # todo: implement me
proc createYieldPermission(): YieldPermission = YieldPermission()
# differen module
proc main() =
setCallback(proc () =
echo "hello world!"
)
doCallback() # ok!
# jield() # correctly prevented, you don't have the YieldPermission
proc yieldable(y: YieldPermission) =
jield y # it's allowed!
yieldable(createYieldPermission())
main()