Skip to content

Latest commit

 

History

History
136 lines (114 loc) · 4 KB

containers-without-generics.md

File metadata and controls

136 lines (114 loc) · 4 KB

A type system without "full generics", and specifically only monomorphizing container definitions (similar to how map and [] work "magically" in Go), but permitting:

  • user-defined containers (map, vector, other)
  • ...with (limited) covariance
  • Possibly no contravariance?

A container definition is automatically given RTTI for the element types (including keys, put possibly also recursively, e.g. map[A]map[B]C could have separate RTTI for A, B, and C?)

Containers must define iteration and lookups, but there must be a compiler-feature to wrap the key and item types with an RTTI wrapper

Pattern matching is needed to go from a container with RTTI to a concrete container type.

Covariance for function signatures, but not for expressions: i.e. these dynamically-typed containers would not follow the Liskov principle.


some useful containers for users to define:

  • maps (with various means of lookups, e.g. BTree, alternate hash algos)
  • vector
  • LRU
  • ordered map
  • set
  • ring buffer

Don't want users to define:

  • allocation
  • memory address locations

Possibly want to permit control over:

  • reallocation trigger
  • reallocation growth rate

What are the value semantics for the containers? Can users control that?


type PMap container<A:Hash,B> {
  // implicit: RTTI for A,B
  // `std` is some standard-lib type for dynamically-growing memory;
  // it uses the same interface as user-defined containers ([], []=, iter)
  // ...it is a sparsely-populated ring-buffer, i.e. effectively
  // Vec<Option<(A,*B)>> with iteration starting at an arbitrary index
  data: ringbuf<(A,*B)>,
  // implicit: `size()` -- ...how does this work? Must be separate
  // counter...right? Or defer to `data`?
}

In expressions, PMap means "dynamically-typed PMap", i.e. with an RTTI pointer. PMap<foo,bar> means a concrete PMap type.

Assignment only works with PMap<foo,bar>. PMap->PMap<foo,bar> can be done with pattern matching.

These are the "magic" functions for containers (using Ctr<A,*B> as a user-defined name):

  Ctr[A] -> Option<*B>
  // `gen` is a keyword for a generator
  // Note: `iter` (especially for std types) should never return
  // "empty"/"missing" items (e.g. for a sparsely-populated vector, the empty
  // items are skipped)
  Ctr.iter() -> gen<A, *B>
  Ctr[A]=*B
  // ...optionally:
  Ctr.resize(usize)
  Ctr.index_iter() -> gen<A>
  Ctr.iter_from(A) -> gen<(A, *B)>
  Ctr.has(A) -> bool
  Ctr[A..A] -> gen<*B>
  // For sparse containers to find "next empty" from an index, and populate it
  Ctr.set_next(A, *B) 
  // For non-assiociative containers (type `<usize,*B>`):
  Ctr.append(*B)
  Ctr.extend(gen<*B>) // ... `gen` should have size-hint when possible

...other than syntax, what's "magic" about these? In particular, are the ones with normal function-call syntax "magic" at all?

User-defined functions:

func (p *PMap<A,B>)[index A] -> Option<*B> {
  let start = index.hash() % p.data.size();
  // ...I don't actually know what the algorithm should be, e.g. how it works
  // in Python. Is there a limit to how many entries to check?
  // ...what should the syntax be for "looping around" the ring buffer?
  for (a, b) := p.data.iter_from(start) {
    if a == index {
      return Some(b)  // is explicit 'Some' necessary?
    }
  }
  return None // is there a more convenient way than explicit Some/None?
}

func (p *PMap<A,B>)[index A]=(value *B) {
  let start = index.hash() % p.data.size();
  p.data.set_next(start, value)
}

// `gen`: keyword for "generator"
// Note: this basically just "defers" to `p.data`. Could use a special syntax for that
func (p *PMap<A,B>)iter() -> gen<A, *B> {
  for (a, b) := p.data.iter() {
    yield (a, b)
  }
}

Using PMap:

func DoStuff(p PMap) {
  for (a, b) := p.iter() {
    // ...a & b are of type `any` here; they are "wrapped" in interface types
  }
}

func DoSomethingSpecific(p PMap) {
  match p {
  case PMap<string, PMap<string, any>>:
    new_map := PMap<string, any>{}
    new_map["inner"] = 37.0
    p["foo"] = new_map
  default:
    ...
  }
}