Currently, I’m attempting (*without success—logic errors remain) to use/abuse Stream.scan
for this:
defmodule HilbertCurve do
use Bitwise
def point(d, order, ndims \\ 2) do
# https://github.com/galtay/hilbertcurve/blob/v2.0.5/hilbertcurve/hilbertcurve.py#L115-L149
# side_length = 2 ** order
# volume = side_length ** ndims
d
|> transpose(order, ndims)
|> gray_decode()
|> undo_excess_work(order)
end
def dist(x, order) do
# https://github.com/galtay/hilbertcurve/blob/v2.0.5/hilbertcurve/hilbertcurve.py#L201-L241
x
|> inverse_undo_excess_work(order)
|> gray_encode()
|> untranspose(order)
end
def gray_encode(x) do
# https://en.wikipedia.org/wiki/Gray_code?oldid=1077948578#Constructing_an_n-bit_Gray_code
Enum.map(Stream.zip(x, [0 | x]), fn {a, b} -> Bitwise.bxor(a, b) end)
end
def gray_decode(x) do
# https://en.wikipedia.org/wiki/Gray_code?oldid=1077948578#Constructing_an_n-bit_Gray_code
Enum.scan(x, 0, &Bitwise.bxor/2)
end
def transpose(i, order, ndims) do
# https://github.com/galtay/hilbertcurve/blob/v2.0.5/hilbertcurve/hilbertcurve.py#L85-L97
#TODO: figure out what this function does
i
|> to_bits(order * ndims)
|> Stream.chunk_every(ndims)
|> Stream.zip()
|> Stream.map(&Tuple.to_list/1)
|> Enum.map(&from_bits/1)
end
def untranspose(x, order) do
# https://github.com/galtay/hilbertcurve/blob/v2.0.5/hilbertcurve/hilbertcurve.py#L100-L112
#TODO: figure out what this function does
x
|> Stream.map(fn i -> to_bits(i, order) end)
|> Stream.zip()
|> Enum.map(&Tuple.to_list/1)
|> List.flatten()
|> from_bits()
end
def undo_excess_work(x, order) do
# https://github.com/galtay/hilbertcurve/blob/v2.0.5/hilbertcurve/hilbertcurve.py#L134-L147
# ndims = length(x)
{_, x} = scan_last(
((order - 2)..0),
{hd(x), x},
fn cur, acc ->
# The first field of `acc` is the x_0 accumulator
# The second field of `acc` is the current value of `x`
r = cur
{x_0, x} = acc
{list__x_0, reversed_list__x_i} = scan_unzip(
Enum.reverse(x),
{x_0, nil},
fn cur, acc ->
# The first field of `acc` is the x_0 accumulator
# The second field of `acc` is the updated value of `x[i]`
x_i = cur
{x_0, _} = acc
uew_op(r, x_i, x_0)
end
)
x_0 = List.last(list__x_0)
x = Enum.reverse(reversed_list__x_i)
{x_0, x} # return
end
)
x # return
end
def uew_op(r, x_i, x_0) do
# https://github.com/galtay/hilbertcurve/blob/v2.0.5/hilbertcurve/hilbertcurve.py#L139-L146
# Returns updated values of {x_0, x_i}
bit_r = 1 <<< r
low_bits = bit_r - 1
if (x_i &&& bit_r) == 0 do
swap_bits({x_0, x_i}, low_bits)
else
{invert_bits(x_0, low_bits), x_i}
end
end
def invert_bits(i, mask \\ -1) do
Bitwise.bxor(i, mask)
end
def swap_bits({a, b}, mask \\ -1) do
# https://github.com/galtay/hilbertcurve/blob/v2.0.5/hilbertcurve/hilbertcurve.py#L143-L146
t = Bitwise.bxor(a, b) &&& mask
a = Bitwise.bxor(a, t)
b = Bitwise.bxor(b, t)
{a, b}
end
def inverse_undo_excess_work(_x, _order) do
# https://github.com/galtay/hilbertcurve/blob/v2.0.5/hilbertcurve/hilbertcurve.py#L215-L226
:TODO
end
# def _to_bits(i, width) do
# <<i::size(width)>>
# end
def scan_last(iterable, initial_acc, f) do
Stream.scan(iterable, initial_acc, f)
|> Enum.take(-1)
|> hd()
end
def scan_unzip(iterable, initial_acc, f) do
Stream.scan(iterable, initial_acc, f)
|> Enum.unzip()
end
def to_bits(i, width) do
i
|> to_bitstring(width)
|> String.to_charlist()
|> Enum.map(&(&1 - hd '0'))
end
def to_bitstring(i, width \\ 0) do
Integer.to_string(i, 2)
|> String.pad_leading(width, "0")
end
# def from_bits(bs) when is_bitstring(bs) do
# padding_size = 7 - rem(:erlang.bit_size(bs) + 7, 8)
# padding = <<0::size(padding_size)>>
# :binary.decode_unsigned(<< padding, bs :: bitstring >>)
# end
def from_bits(l) when is_list(l) do
Enum.map(l, &(&1 + hd '0'))
|> List.to_string()
|> from_bitstring()
end
def from_bitstring(s) do
Integer.parse(s, 2)
|> (fn {i, ""} -> i end).()
end
end
#IO.puts("The following should be 1234567:")
#IO.inspect(HilbertCurve.dist([6, 1, 6, 5, 6, 3, 6], 4))
IO.puts("The following should be [1, 2, 3, 4, 5, 6, 7]:")
IO.inspect(HilbertCurve.point(2000640, 4, 7))
My main worry is that this isn’t “in the spirit of” how functional programming is supposed to be done—I feel like I’m just trying to blindly imitate the for
loops rather than getting at what’s actually happening at the math level—and that my wrong perspective in the translation is what’s ultimately leading to my difficulties in getting the logic errors out.
(edit: I do intend to mark the internal functions private once it’s all working, but marking them all as public is extremely handy for debugging)