the simplest io

the simplest io
Photo by CARTER SAUNDERS / Unsplash

Let's try and write the simplest io library - moving objects from one array to another; such as characters in a string. One at a time. We can look at bulk later.

input: Range = {start: 1, stop: 10}.
output: Array[Integer, 10].

// example:
input map> [+ 1] copy> output.

// left associative, this is:
(input map> [+ 1]) copy> output.

The map> method won't actually do anything because there's no destination yet. The copy> method will perform an actual operation moving data from its source to its destination. Let's begin there.

Readable :: {#interface}.
Writeable :: {#interface}.

[self read]
  [&Readable -> self element-class / IOStatus |
    #required].

[self write: element]
  [&Writeable -> self element-class / IOStatus |
    #required].

[src copy> dst]
  [&Readable, &Writeable -> return: ø |
    dst write: (src read else: [return ø]).
    src copy> dst].

This is very naive. We're not handling potential error states yet; just aborting when read fails. Let's try and improve this.

IOStatus :: {OK, OutOfData, OutOfSpace, Fault}.
[src copy> dst]
  [&Readable, &Writeable -> return: IOStatus |
    element: src element-class = src read else: [failure | return failure].
    (dst write: element) else: [failure | return failure].
    src copy> dst].

Cool. Let's see if we can implement read and write. For an Array we're going to need an offset in to it. We need to wrap up the Array in to a readable or writeable thing.

ArrayIterator
:: {...Readable, ...Writeable,
    array: Array, offset: 0}.

[src copy> dst]
  [&Readable, &Array[src element-class] -> IOStatus |
    src copy> {ArrayIterator | array: dst}].

[self read]
  [&ArrayIterator -> return: self element-class / IOStatus |
    offset = array length then: [return OutOfData].
    element: array[offset].
    offset := offset + 1.
    return element].

[self write: element]
  [&ArrayIterator, self element-class -> IOStatus |
    array[offset] = element.
    offset := offset + 1].

By adding both read and write we're almost there to running this. Let's implement map. Map is like ArrayIterator but it's a transformation and doesn't know about its types or offsets or anything like that.

MappingIterator
:: {...Readable, ...Writable,
    #src-element-class, #dst-element-class: Class,
    target: Readable,
    op: #src-element-class -> #dst-element-class / IOStatus}.

[src map> op]
  [&Array,
   (src element-class -> operation return-class / IOStatus),
   MappingIterator from: src element-class to: operation return-class) |
     ({ArrayIterator | array: src}) map> op].

[src map> op]
  [&Readable,
   (src element-class -> operation return-class / IOStatus),
   (MappingIterator from: src element-class to: operation return-class) |
     {src: src, op: op}].

[self read]
  [MappingIterator -> failure: self dst-element-class / IOStatus |
    element := target read else: [failure | return failure].
    return (op evaluate: element)].

[self write: element]
  [MappingIterator, self src-element-class -> return: IOStatus |
    mapped := (op evaluate: element) else: [failure | return failure].
    return (target write: mapped)].

Weirdly - that's it. We now have a working IO system for moving objects between arrays. We can now add other kinds of sources and destinations. We can also look at doing operations in bulk.

When doing simple object transformations like this bulk doesn't buy us much. But the moment we start using sockets we can send/receive in chunks. When we start playing with SIMD we can transform in chunks. We want a loop inside each read and write operation. We'll need to change our API.

We can do that in the next post.