I’ve been using Python for a while and playing with its recent static type system.
When I discovered ways to do type-level programming (e.g. I implemented a list that knows its length, updates it correctly, does bounds checking, etc… all at type-checking time) I was told that I was “abusing” Python’s type system.
Indeed, I discovered some interesting tricks that then went away in later versions of the type checker I was using. The author of that type checker was quite dismissive of my work, so I gave up instead of looking for eventual workarounds.
TypeScript’s type system, although unsound, is quite powerful for type-level programming (e.g. I implemented a simple language in it (i.e. parser + interpreter)), but, unfortunately, I don’t like JS very much.
Before diving into Scala, I’d like to know whether what I want to do is possible or not (but I’ll probably move to Scala anyway).
Instead of avoiding mutability, I’d like to control it. I don’t need anything too fancy. I’d simply like to split classes’ (or types’, in general) interfaces into at least 2 (overlapping) versions: read-only (r) and read-write (w).
(Sorry for using Python terminology.) As an example, let’s say we have a list[int]
. After the split, we get list_r[int]
and list_w[int]
. The former is read-only and the latter read-write. Note that, since list
is a pre-existing type, we’ll need a lift
function to lift a regular list
to a list_*
. Now let’s define the following function:
def f(xs: list_r[int], ys: list_w[int]) -> list_w[int]:
...
Here’s an example of the behavior I want:
rl: list_r[int] = lift([1, 2, 3]) # also `lift([1, 2, 3], 'r')`
wl: list_w[int] = lift([4]) # also `lift([4], 'w')`
f(rl, rl) # error: can't convert a list_r into a list_w
f(rl, wl) # error: write access needs explicit permission
f(rl, w(wl))
f(wl, w(wl)) # note the w -> r coercion
f(rl, w(rl)) # error: can't convert a list_r into a list_w
Also, note that list_r[T]
is covariant in T
, as it should.
The actual system had 4 access modes:
r
: read-only accessw
: read-write accessrk
: keep in read-only modewk
: keep in read-write mode
“keep” tells a function that it can keep the value. Without “keep”, a function must completely forget about that value before returning. This holds recursively, of course.
This is also useful to eliminate redundant copies. For instance, a constructor may require a list_wr[T]
, which means that it may keep and use the list indefinitely, without having to make a copy.
The read-only
and read-write
qualities are actually enforced:
def f(xs: list_r[int], ys: list_w[int]) -> list_w[int]:
xs.sort() # error: no in-place sorting allowed
xs[0] = 4 # error
...
ys.sort() # error: permission needed
w(ys).sort()
w(ys)[0] = 3
return xs # error: can't convert list_r to list_w
return xs.copy() # NOTE: `copy` always returns a list_w
Note that, at runtime, list_r
and list_w
(and list_rk
and list_wk
) are just regular lists, and lift
and w
just identity functions. This is all just a static type mechanism.
The “permission part” is the trickiest to implement. I used an ugly hack:
@lock
def f[L](_: L, xs: list_r[int], ys: list_w[int, L]) -> list_w[int, L]:
...
I use lock
to create an “external” signature for f
. The callers will see the following signature:
def f(xs: list_r[int], ys: list_w[int, _Unlocked]) -> list_w[int, None]
L
is a type param for a “lock”. When created by lift
or directly by list_w
, L
is just None
. To appease the external signature, the caller must use w
for ys
, which converts ys
from a list_w[int, None]
to a list_w[int, _Unlocked]
. Note that the return type must have L = None
or that list_w
would be pre-authorized, which is something we don’t want.
OTOH, the implementation of f
sees a generic list_w[int, L]
and so it must use w
to pass ys
to other functions. In particular, f
can’t call itself by forwarding its args:
@lock
def f[L](_: L, xs: list_r[int], ys: list_w[int, L]) -> list_w[int, L]:
f(xs, ys) # type error: needs explicit write permission
f(xs, w(ys))
Of course, the example above recurses forever, but that’s beside the point
In case you’re wondering, that _: L
is a hack I need to get a hold of the type param in lock
so that I can specialize it into _Unlocked
for the args, and None
for the return value. In Python there’s no way to manipulate a signature in a general way.
I can’t get the latest version of the type checker to replace L
with the intended types anymore, which broke my code That’s apparently my fault, since I was relying on undefined behavior. In my defense, most of Python type system’s behavior is undefined. Unless I’m doing really basic stuff, I always get different results with different type checkers. I’ve had enough.
BTW, implementing w
(+ r
, rk
and wk
) is easy enough, but lift
is problematic because it operates on a type we don’t control. The obvious solution is function overloading (list → list_w, set → set_w, etc…), but it’s an inefficient one, as overloading resolution is usually cubic in the number of overloads, and, even worse, one would’ve to import all the supported types at once. I opted for local lift
s. For instance, let’s say a file uses list
, set
, and dict
. We just create a local lift
as follows:
lift = Lift[list_w | set_w | dict_w]()
Lift
’s implementation is full of “abuses”, of course!
Hoping you’re still with me, my question is the following:
Can I implement something like that in Scala?