6

Whereby I could construct an anonymous, ad-hoc record; that's editable, appendable, modifiable, where each value can have different heterogenous type, and where the compiler checks that the consumers type expectations unify with the types of the produced record at all the given keys?

Similar to what Purescript supports.

Wizek
  • 4,854
  • 2
  • 25
  • 52

1 Answers1

8

It could, but there isn't a module in the standard library, and the two github projects gonzaw/extensible-records and jmars/Records don't seem to be full-fledged/outdated.

You might need to implement it for yourself. The rough idea is:

import Data.Vect

%default total

data Record : Vect n (String, Type) -> Type where
  Empty : Record []
  Cons : (key : String) -> (val : a) -> Record rows -> Record ((key, a) :: rows)

delete : {k : Vect (S n) (String, Type)} -> (key : String) ->
       Record k -> {auto prf : Elem (key, a) k} -> Record (Vect.dropElem k prf)
delete key (Cons key val r) {prf = Here} = r
delete key (Cons oth val Empty) {prf = (There later)} = absurd $ noEmptyElem later
delete key (Cons oth val r@(Cons x y z)) {prf = (There later)} =
  Cons oth val (delete key r)

update : (key : String) -> (new : a) -> Record k -> {auto prf : Elem (key, a) k} -> Record k
update key new (Cons key val r) {prf = Here} = Cons key new r
update key new (Cons y val r) {prf = (There later)} = Cons y val $ update key new r

get : (key : String) -> Record k -> {auto prf : Elem (key, a) k} -> a
get key (Cons key val x) {prf = Here} = val
get key (Cons x val y) {prf = (There later)} = get key y

With this we can write functions that handle fields without knowing the full record type:

rename : (new : String) -> Record k -> {auto prf : Elem ("name", String) k} -> Record k
rename new x = update "name" new x

forgetAge : Record k -> {auto prf : Elem ("age", Nat) k} -> Record (dropElem k prf)
forgetAge k = delete "age" k

getName : Record k -> {auto prf : Elem ("name", String) k} -> String
getName r = get "name" r

S0 : Record [("name", String), ("age", Nat)]
S0 = Cons "name" "foo" $ Cons "age" 20 $ Empty

S1 : Record [("name", String)]
S1 = forgetAge $ rename "bar" S0

ok1 : getName S1 = "bar"
ok1 = Refl

ok2 : getName S0 = "foo"
ok2 = Refl

Of course you can simplify and prettify this alot with syntax rules.

xash
  • 3,702
  • 10
  • 22
  • This is cool! Thanks for writing it up. I'm glad to see that it type-checks. I'll be testing this concept a bit, and trying to understand it. I hope it doesn't suffer from similar limitations as Haskell's Bookkeeper/rawr/superrecord and similar extensible record libraries, e.g. very limited key count is supported and compile times suffer superlinearly. Would you happen to know if so or not? – Wizek Apr 19 '18 at 11:12
  • I guess they implement it similiar: with a linked list of (key, type). The type penalty comes mostly from the fact that the compiler needs to construct `{auto prf : Elem …}`, that (key, type) is actually in the list, and then uses this proof to find the value. So it traverses the list two times and building an intermediate proof for it. This can be optimized, I guess, so it actually uses the key to find a value. Anyway, I'm not sure how slow Haskell's libraries are, but `getName $ rename "test" S` with `S` being a record with 100 keys takes a minute to compile, with 10 keys under a second. – xash Apr 19 '18 at 12:09
  • 2
    Changing `Record` from `Vect n (String, Type) -> Type` to `List (String, Type) -> Type` makes this apperently linear and even 100 rows take only seconds for the compiler to check, though you have to reimplement `Elem` for List. – xash Apr 19 '18 at 13:08
  • hmm, seconds does sound better than minutes, though it's a bit concerning for me that it even takes that much. does it have to check so many things? – Wizek Apr 19 '18 at 20:26
  • I want to clarify my previous comment a bit, I guess I was asking a few questions at once. Is it taking even a second because the type checker has to check lots of things? If so, can I somehow see what it checks? E.g. with some kind of profiling. And finally, do you think it could be reduced even further? E.g. lowering the constant factor or even making it sublinear? Or something more fundamental might be at play here? – Wizek Apr 19 '18 at 22:13
  • 2
    I'm only guessing here: this implementation is basically a list, so if you keep it so, it won't become sublinear. But you could implement records as trees, making things O(log n). The time is mostly consumed by the type checker, but Idris has yet no tools for debugging/optimizing for speed. So if you need a fast library to reason about large records at compile time, you'll have to wait. :-) – xash Apr 20 '18 at 11:15