Stream: beginners

Topic: Is there a better way to write this simple function?


view this post on Zulip Matthieu Pizenberg (May 09 2024 at 18:58):

I’m not looking for production-grade code, mainly is it idiomatic or is there more idiomatic way to write that function computing the Caesar Cipher of a string?

expect caesar "abc" 1 == Ok "bcd"
expect caesar "abc" 25 == Ok "zab"

caesar : Str, U8 -> Result Str _
caesar = \plaintext, shift ->
    Str.walkUtf8 plaintext [] \acc, letter ->
        List.append acc ((letter - 'a' + shift) % 26 + 'a')
    |> Str.fromUtf8

view this post on Zulip Hristo (May 09 2024 at 19:18):

I can't answer your question regarding whether the following may be more idiomatic or not, but if your expected input is only ASCII, you might consider also (in pseudocode):

  1. Convert the string to List U8.
  2. Split at a particular index, based on shift.
  3. Concatenate the halves accordingly.
  4. Join back into a string.

I suspect this version might be a tiny bit more readable (as you wouldn't need to do the element-wise shifting explicitly).

view this post on Zulip Matthieu Pizenberg (May 09 2024 at 19:31):

Indeed that’s more readable to convert to List U8 first, thanks @Hristo

caesar : Str, U8 -> Result Str _
caesar = \plaintext, shift ->
    Str.toUtf8 plaintext
    |> List.map \c -> (c - 'a' + shift) % 26 + 'a'
    |> Str.fromUtf8

view this post on Zulip Matthieu Pizenberg (May 09 2024 at 19:33):

I’m not sure what you meant by split and concat though

view this post on Zulip Brendan Hansknecht (May 09 2024 at 19:34):

The list version should also be a lot more performant

view this post on Zulip Brendan Hansknecht (May 09 2024 at 19:34):

It should happen inplace

view this post on Zulip Hristo (May 09 2024 at 20:20):

@Matthieu Pizenberg you're absolutely right!

I’m not sure what you meant by split and concat though

I wasn't being clear at all in my message above. But I'm also glad that - despite my confusing pseudocode - you arrived at a version which you feel is better.

I was thinking in terms of the Caesar cipher being an explicit mapping, like so:

alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']

ceasar : Str, U8 -> Str
ceasar = \plaintext, shift ->
    mapChar = ceasarHelp shift
    Str.toUtf8 plaintext
    |> List.map mapChar
    |> Str.fromUtf8
    |> Result.withDefault ""

ceasarHelp : U8 -> (U8 -> U8)
ceasarHelp = \shift ->
    { before, others } = List.split alphabet (Num.toU64 shift)
    shifted = List.concat others before
    zipped = List.map2 alphabet shifted Pair
    dict = List.walk zipped (Dict.empty {}) \mapping, (Pair input output) ->
        Dict.insert mapping input output
    \input -> Dict.get dict input |> Result.withDefault 0

expect ceasar "abc" 1 == "bcd"
expect ceasar "abc" 25 == "zab"

Clearly, this isn't as readable - I have to admit that prior to writing it out, the mapping part seemed (in my mind) it'd be neater to implement and subsequently read.

Thanks @Brendan Hansknecht for jumping in to confirm regarding idiomaticity and efficiency!

view this post on Zulip Matthieu Pizenberg (May 09 2024 at 20:27):

Right! yes I was missing the alphabet part. I like that version too thanks for that. Not as terse but shows more of the language

view this post on Zulip Brendan Hansknecht (May 09 2024 at 20:38):

This last version will probably be a lot slower. Dict in the hot loop will be a lot slower than a little bit of math.

view this post on Zulip Hristo (May 09 2024 at 20:50):

Yeah, that's true! :100:
It might be more useful in cases where arbitrary mappings are needed (instead of something that could be computed arithmetically).

view this post on Zulip Matthieu Pizenberg (May 09 2024 at 21:03):

Right, that’s more useful in settings where the substitution key of the cipher is arbitrary


Last updated: Jul 06 2025 at 12:14 UTC