Stream: beginners

Topic: ✔ Grids and Parsing - Roc idioms (Exercism OCR Numbers)


view this post on Zulip Jonathan (Feb 19 2025 at 14:19):

To get a little more Roc familiarity (very little hands-on experience) I have been trying the Exercism track, and got a bit stuck on the OCR numbers exercise. I'm interested in seeing what techniques people would reach for to solve this? I don't believe it's complicated, but felt like I was forcing Roc in a direction that was unnatural. I can't apologise for being a beginner, but I'm sorry if the snippets below are not great!

Grids have been a pain point for me in statically typed functional languages, and in this case the problem is compounded by having a subgrid of ASCII number representations within a grid of numbers described by the string, each to be parsed into the number they represent.

      _  _     _  _  _  _  _  _  #
    | _| _||_||_ |_   ||_||_|| | # decimal numbers.
    ||_  _|  | _||_|  ||_| _||_| #
                                 # fourth line is always blank

I supposed I could represent each row as U8s, use chunks_of to get the groups of 3 (the width of each number), then transpose and chunk again by 4 (the height). My problem now was proving to the type system that that there were groups of 12 elements, which is where I'd usually reach for multiple clauses and list destructuring to declaratively handle each case.

As (I don't believe?) Roc has list destructuring, I instead went the route of defining chunks3 and 4, to return 3/4-tuples the type system can reason with, (each requiring multiple list gets and result destructuring instead):

chunk3 : List a -> List (a, a, a)
chunk3 = |l|
    first = List.get(l, 0)
    second = List.get(l, 1)
    third = List.get(l, 2)
    when (first, second, third) is
        (Ok(a), Ok(b), Ok(c)) -> List.prepend(chunk3(List.drop_first(l, 3)), (a, b, c))
        _otherwise -> []

with the final endpoint (after converting the nested tuples to a binary representation to make pattern matching less noisy) as such:

    when s is
        # gfedcba
        0b0111111 -> Ok(0)
        0b0000110 -> Ok(1)
        # ...
        _ -> Err(InvalidPattern)

This just felt bad, and I feel like I have either missed some standard library functions, or not adapted to alternative methods (or just written some very bad code!!).

Transposing a nested list was also not papercut free (using the standard map head concatenated onto recursive transpose tail approach) as it also required a helper function to prove that the list had a head available.

transpose : List (List a) -> List (List a)
transpose = |nested_list|
    head_tail = map_head_tail(nested_list)
    when head_tail is
        Err(UnequalLength) -> []
        Ok((first, tails)) -> List.concat([first], transpose(tails))

map_head_tail : List (List a) -> Result (List a, List (List a)) [UnequalLength]
map_head_tail = |nested_list|
    firsts = List.map(nested_list, List.first)
    all_ok = List.all(firsts, |r| Result.is_ok(r))
    if Bool.not(all_ok) then
        Err(UnequalLength)
    else
        ok_firsts = List.keep_oks(firsts, |x| x)
        tails = List.map(nested_list, |l| List.drop_first(l, 1))
        Ok((ok_firsts, tails))

I would really appreciate any pointers - Roc specific or general - for parsing and dealing with these kinds of structures idiomatically!

view this post on Zulip Jonathan (Feb 19 2025 at 14:25):

If someone could also help me understand the precedence of types in WSA type annotations too - List List a does not behave as List (List a), but I'm having a hard time figuring out what the former could mean

view this post on Zulip Brendan Hansknecht (Feb 19 2025 at 15:52):

Roc has list destructuring:

when list is
    [a, b, c, .. as rest] -> ...
    _ -> ...

view this post on Zulip Jonathan (Feb 19 2025 at 15:55):

That's embarrassing! No idea how I missed that in the tutorial, thank you!!

view this post on Zulip Jonathan (Feb 19 2025 at 15:55):

Will re-impliment in a more sane manner and see how it goes :laughing:

view this post on Zulip Jonathan (Feb 19 2025 at 15:57):

Docs even say

This can be both more concise and more efficient (at runtime) than calling List.get multiple times

:man_facepalming: :man_facepalming: :man_facepalming:

view this post on Zulip Brendan Hansknecht (Feb 19 2025 at 15:57):

List List a is definitely a bug. It is essentially equivalent to Dict k v in parsing. It is a List type with 2 type variables (which already should fail cause list takes only one type variable). The first type variable is List without any type variable (also a bug). The second type variable is a.

I think roc is missing the check on type variable count which often leads to weird failures with types like this.

view this post on Zulip Jonathan (Feb 19 2025 at 16:04):

Ah ok, thanks. I wasn't sure because it wasn't giving me any errors, and was resulting in some weird type signatures from the LSP that I do not yet understand
image.png
image.png

view this post on Zulip Jonathan (Feb 19 2025 at 16:10):

Anyhow, I suppose these (bugs?) are moot with the compiler rewrite

view this post on Zulip Notification Bot (Feb 19 2025 at 16:11):

Jonathan has marked this topic as resolved.

view this post on Zulip Brendan Hansknecht (Feb 19 2025 at 16:50):

Jonathan said:

Anyhow, I suppose these (bugs?) are moot with the compiler rewrite

yeah


Last updated: Jul 06 2025 at 12:14 UTC