Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mutable references and arrays #126

Merged
merged 6 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
351 changes: 351 additions & 0 deletions lib/Mutable.fram
Original file line number Diff line number Diff line change
@@ -0,0 +1,351 @@
{# This file is part of DBL, released under MIT license.
# See LICENSE for details.
#}

## # Mutable values and arrays

{##
Mutable values and arrays in Fram are not a part of the language, but they are
defined as a library. This means that programming with mutability requires
a small but reasonable syntactic overhead. The following example shows how to
use mutability in Fram.
```
import open Mutable

{# Create an immutable array filled with the first n primes. #}
pub let primes n =
{# In order to use mutability, we need a `Mut` capability. There is a
predefined `ioMut` capability, but using it performs the `IO` effect.
Mutability can be handled locally, by introducing a local capability,
but the computation that uses it must be pure, as in this example. #}
hMutArray (fn mut =>
{# New mutable entities are created by calling methods on the `Mut`
capability. #}
let arr = mut.makeArray n 2
let lastPrime = mut.ref 1
let isPrime k =
{# The whole computation must be pure, but it can use other effects
locally. #}
handle ret = effect b => b
return () => False
let checkPrime (p : Int) =
if p * p > k then ret True
else if k % p == 0 then ret False
in
arr.iter checkPrime
let rec nextPrime () =
{# Mutable entities can be accessed by the `get` method, #}
let p = lastPrime.get + 1 in
{# or modified using the `:=` operator. #}
lastPrime := p;
if isPrime p then p
else nextPrime ()
in
arr.iteri (fn i _ =>
{# To modify an array, we also use the `:=` operator, but on a special
write-only element that can be obtained with the `at` method. To read
an array, we use the `get` method. #}
arr.at i := nextPrime ());
arr)
```
##}

import List
ppolesiuk marked this conversation as resolved.
Show resolved Hide resolved

{# ========================================================================= #}
## ## General definitions

data RefEx = RefEx of
{ Ref : effect -> type -> type
, Array : effect -> type -> type
, ConstArray : type -> type
}

pub let RefEx { Ref, Array, ConstArray } =
(extern dbl_abstrType : Unit ->[IO] RefEx) ()

{## Infix operator for setting mutable values. ##}
pub method fn (:=) = set

{## Abstract capability of mutability. ##}
abstr data Mut (effect E) = Mut

{## IO capability of mutability.

The IO effect is rich enough to describe computations that use mutability.
Therefore, this capability is always available. ##}
pub let ioMut = Mut {effect=IO}

{## Local handler of state.

The mutability effect can be handled locally, provided that the handled
computation has no other effects. ##}
pub let hMut (f : {E : effect} -> Mut E ->[E] _) =
handle {effect=E} _ = () in
f {E} Mut

{## Local handler of state, translated to IO.

This function is similar to `hMut`, but it allows the handled expression to
have other effects. However, this function is not pure: it has the `IO`
effect. ##}
pub let hMutIO (f : {E : effect} -> Mut E ->[E|_] _) =
f {E=IO} ioMut

{# ========================================================================= #}
## ## Mutable references

{## @type Ref
Mutable references, annotated with the effect of reading and writing. ##}

{## Get the contents of a mutable value. ##}
pub method get {E, type T, self : Ref E T} =
(extern dbl_refGet : Ref E T ->[E] T) self

{## Set the contents of a mutable value. ##}
pub method set {E, type T, self : Ref E T} =
(extern dbl_refSet : Ref E T -> T ->[E] Unit) self

{## Create a new mutable cell. ##}
pub method ref {E, type T, self : Mut E} =
extern dbl_ref : T ->[E] Ref E T

{# ========================================================================= #}
## ## Mutable arrays

{## @type Array
Mutable arrays, annotated with the effect of reading and writing. ##}

{## Write-only elements of an array.

See the `at` method of the `Array` type for details. ##}
pub data ArrayElem E T = { set : T ->[E] Unit }

let unsafeGetArray {E, type T} =
extern dbl_arrayGet : Array E T -> Int ->[E] T

let unsafeSetArray {E, type T} =
extern dbl_arraySet : Array E T -> Int -> T ->[E] Unit

let unsafeMakeArray {E, type T} =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we are using ocaml for evaluation, we might want to restrict length of arrays, to one we are allowed to create in ocaml (Sys.max_array_length).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. But what should we do when the user tries to create too large array?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a discussion @ppolesiuk suggested that we could take an approach similar to OCaml and expose the maximum array length for when the programmer requires this kind of assurance. I think this is reasonable, given that oversized allocations are uncommon and recovery from out of memory situations is rarely needed/desirable (at least for such a high level language). However, perhaps we still want to check the size somewhere, to display a custom error instead of OCaml's.

extern dbl_mkArray : Int ->[E] Array E T

{## Get the length of an array. ##}
pub method length {E, self : Array E _} =
(extern dbl_arrayLength : Array E _ -> Int) self

{## Get the nth element of an array.

This function assumes that the given index is within array bounds. ##}
pub method get {E, type T, self : Array E T} (n : Int) =
assert {msg="Index out of bounds"} (n >= 0 && n < self.length);
unsafeGetArray {E} self n

{## Set the nth element of an array.

This function assumes that the given index is within array bounds. ##}
pub method setAt {E, type T, self : Array E T} (n : Int) v =
assert {msg="Index out of bounds"} (n >= 0 && n < self.length);
unsafeSetArray {E} self n v

{## Get the nth element of an array in order to write to it.

This method provides a convenient way of writing to an array, as in the
following example.
```
let foo {E} (arr : Array E String) =
arr.at 42 := "example string"
```
##}
pub method at {E, self : Array E _} n =
ArrayElem { E, set = self.setAt n }

{## Convert an array to a list. ##}
pub method toList {E, self : Array E _} =
let rec loop (n : Int) acc =
if n == 0 then acc
else (
let n = n - 1 in
let acc = unsafeGetArray {E} self n :: acc in
loop n acc)
in loop self.length []

{# ------------------------------------------------------------------------- #}
## ### Creating new arrays

let unsafeInitArray {E} n (f : _ ->[E] _) =
let arr = unsafeMakeArray {E} n
let rec loop (i : Int) =
if i < n then (
unsafeSetArray {E} arr i (f i);
loop (i + 1))
in
loop 0;
arr

{## Convert a list to an array. ##}
pub method toArray {E, ~mut : Mut E, self : List _} =
let arr = unsafeMakeArray {E} self.length in
self.iteri (unsafeSetArray {E} arr);
arr

{## Create a new array initialized with the results of a pure function.

A call `mut.pureInitArray n f` creates an array built from elements `f 0`,
..., `f (n-1)`, calling this function from left to right. The function `f`
cannot have effects other than the effect of the `mut` capability. This
function can be used for initializing matrices, e.g.,
```
let delta i j = if i == j then 1 else 0 in
mut.pureInitArray n (fn i => mut.pureInitArray n (fn j => delta i j))
```
If you need to perform other effects in the function `f`, see the `initArray`
method of type `Mut`.

The size of the array provided to this method must be non-negative.
##}
pub method pureInitArray {E, self : Mut E} (n : Int) (f : _ ->[E] _) =
assert {msg="Array of negative size"} (n >= 0);
unsafeInitArray {E} n f

{## Same as `pureInitArray`, but it allows the function `f` to be impure. ##}
pub method initArray {E, self : Mut E} n f =
(List.init n f).toArray {~mut=self}

{## Create an array initialized with the default element.

A call `mut.makeArray n v` is equivalent to `mut.initArray n (fn _ => v)`.
##}
pub method makeArray {E, self : Mut E} n v =
self.pureInitArray n (fn _ => v)

{## Create a copy of a given array.

This method returns a shallow copy, i.e., elements are shared between arrays.
##}
pub method clone {E, type T, self : Array E T} =
unsafeInitArray {E} self.length (unsafeGetArray {E} self)

{# ------------------------------------------------------------------------- #}
## ### Higher order functions on arrays

{## Iterate over all elements of an array and their indices.

In `arr.iteri f` for each element of array `arr` function `f` is applied to
the index of the element (counting from 0) and the element itself. The array
can be modified by the function `f`, but next calls of `f` will be performed
on modified elements. In order to avoid such an odd behavior, the function
can be called on the cloned array: `arr.clone.iteri f`. ##}
pub method iteri {E, self : Array E _} f =
let rec loop (i : Int) =
if i >= self.length then ()
else (
f i (unsafeGetArray {E} self i);
loop (i + 1))
in
loop 0

{## Iterate function over all elements of an array.

Call `arr.iter f` is equivalent to `arr.iteri (fn _ => f)`. ##}
pub method iter {E, self : Array E _} f =
self.iteri (fn _ => f)

{# ========================================================================= #}
## ## Immutable arrays

{## @type ConstArray
Immutable arrays.

Immutable arrays cannot be modified, but accessing their elements is pure.
Immutable arrays are not parametrized by an effect.
##}

let unsafeGetConstArray {type T} =
extern dbl_arrayGet : ConstArray T -> Int ->[] T

{## Get the length of an immutable array ##}
pub method length {self : ConstArray _} =
(extern dbl_arrayLength : ConstArray _ -> Int) self

{## Get the nth element of an immutable array.

This function assumes that the given index is within array bounds. ##}
pub method get {type T, self : ConstArray T} (n : Int) =
assert {msg="Index out of bounds"} (n >= 0 && n < self.length);
unsafeGetConstArray self n

{## Convert an immutable array to a list ##}
pub method toList {self : ConstArray _} =
let rec loop (n : Int) acc =
if n == 0 then acc
else (
let n = n - 1 in
let acc = unsafeGetConstArray self n :: acc in
loop n acc)
in loop self.length []

{# ------------------------------------------------------------------------- #}
## ### Creating new immutable arrays

let unsafeFreeze {E, type T} (arr : Array E T) =
(extern dbl_magic : Array E T -> ConstArray T) arr

{## Freeze the contents of mutable array.

This method returns a new immutable array that is a (shallow) copy of given
mutable array. ##}
pub method freeze {E, self : Array E _} =
unsafeFreeze {E} self.clone

{## A handler of mutability, that returns an immutable array.

With this handler it is possible to create an immutable array by a
computation that returns a mutable array and doesn't perform any effects
other than mutability. In such a case the result can be safely frozen
without copying the whole array. The call `hMutArray f` is equivalent
to `hMut (fn mut => f mut >.freeze)`, but faster. ##}
pub let hMutArray (f : {E : effect} -> Mut E ->[E] Array E _) =
handle {effect=E} _ = () in
unsafeFreeze {E} (f {E} Mut)

{## Convert a list to an immutable array. ##}
pub method toConstArray {self : List _} =
hMutArray (fn ~mut => self.toArray)

{## Create a new immutable array initialized with results of a pure function.

A call `mut.pureInitConstArray n f` creates an immutable array built from
elements `f 0`, ..., `f (n-1)`, calling this function from left to right.
The function `f` must be pure. The size of an array provided to this method
must be non-negative.
##}
pub let pureInitConstArray n (f : _ ->[] _) =
hMutArray (fn mut => mut.pureInitArray n f)

{## Same as `pureInitConstArray`, but it allows function `f` to be impure. ##}
pub let initConstArray n f =
(List.init n f).toConstArray

{# ------------------------------------------------------------------------- #}
## ### Higher order functions on immutable arrays

{## Iterate over all elements of an immutable array and their indices.

In `arr.iteri f` for each element of the array `arr` the function `f` is
applied to the index of the element (counting from 0) and the element itself.
##}
pub method iteri {self : ConstArray _} f =
let rec loop (i : Int) =
if i >= self.length then ()
else (
f i (unsafeGetConstArray self i);
loop (i + 1))
in
loop 0

{## Iterate function over all elements of an immutable array.

Call `arr.iter f` is equivalent to `arr.iteri (fn _ => f)`. ##}
pub method iter {self : ConstArray _} f =
self.iteri (fn _ => f)
Loading
Loading