Stream: API design

Topic: Id function


view this post on Zulip Eli Dowling (Jan 03 2024 at 23:44):

I think roc should consider including an "id" function within the builtins set. it's just something that comes up now and again and I've liked having it as a builtin in other languages
it's often useful when some operation is optional, eg:

list |> (if fwd then \a -> a else List.reverse)
#becomes
list |> (if fwd then id else List.reverse)

view this post on Zulip Richard Feldman (Jan 03 2024 at 23:53):

this was an intentional omission actually :big_smile:

view this post on Zulip Richard Feldman (Jan 03 2024 at 23:53):

I've found that in general if an identity function is desirable, it's usually a sign that an API is missing a function

view this post on Zulip Richard Feldman (Jan 03 2024 at 23:54):

if that exact code example is from real-world code, I'd be curious what the larger example looks like!

view this post on Zulip Eli Dowling (Jan 03 2024 at 23:59):

Okay, interesting.
It is from real code, I'm writing a function that searches through a string (a bit of AOC catchup), but it needs to be able to operate forwards and backwards. There are a few places i need to reverse something or not depending on which way we are searching

I've replaced the id function uses with this odd little utility

ifF : a, Bool, (a -> a) -> a
ifF = \input, cond, f -> if cond then f input else input

view this post on Zulip Richard Feldman (Jan 04 2024 at 00:30):

if you're up for sharing the whole code, I'd love to check it out!

view this post on Zulip Eli Dowling (Jan 04 2024 at 00:31):

Here is a big chunk of the code. I'm well aware it's messy and not pretty, but I was trying to solve it with the least iteration possible and make use of breaking out of walks to return early

 numberNames = [
    "zero",
    "one",
    "two",
    "three",
    "four",
    "five",
    "six",
    "seven",
    "eight",
    "nine",
]
nameToNum = \str ->
    when str is
        "zero" -> 0
        "one" -> 1
        "two" -> 2
        "three" -> 3
        "four" -> 4
        "five" -> 5
        "six" -> 6
        "seven" -> 7
        "eight" -> 8
        "nine" -> 9
        _ -> crash "bad"

ifF : a, Bool, (a -> a) -> a
ifF = \inp, cond, f -> if cond then f inp else inp

findFirstNum : List U8, [Forward, Backward] -> U32
findFirstNum = \str, forward ->
    rev = forward != Forward
    # reverse if needed
    names : List List U8
    names =
        numberNames
        |> List.map \name ->
            name
            |> Str.toUtf8
            |> ifF rev List.reverse
    walker = if rev then List.walkBackwardsUntil else List.walkUntil
    finders =
        names
        |> List.map \name ->
            name
            |> WordFinder.fromList
            |> WordFinder.startSearch str
    num =
        str
        |> walker
            { finders, idx: 0, num: 0 }
            \state, char ->

                # BookKepping so we have an index
                idx = state.idx
                nextState = { state & idx: idx + 1 }

                if char >= 0x30 && char <= 0x39 then
                    Break { state & num: (char - 0x30) }
                else
                    # step all our finders forward one
                    nextFinders =
                        state.finders
                        |> List.map \finder ->
                            WordFinder.nextStep finder char idx

                    # see if any matched
                    matched =
                        nextFinders
                        |> List.walkUntil (Err NotFound) \state2, finder ->
                            when WordFinder.firstMatch finder is
                                Ok _start -> Break (Ok (finder |> WordFinder.searchingFor))
                                _ -> Continue state2
                    # If we got a match we can return early
                    when matched is
                        Ok match ->
                            matchNum = match |> ifF rev List.reverse |> Str.fromUtf8 |> Result.withDefault ("") |> nameToNum

                            Break ({ state & num: matchNum })

                        _ -> Continue { nextState & finders: nextFinders }
        |> .num
    num |> Num.toU32

getPair = \inp ->

    inp
    |> List.map \str ->
        strList =
            str
            |> Str.toUtf8
        (strList |> findFirstNum Forward, strList |> findFirstNum Backward)

pairToNum = \(a, b) ->
    (a * 10 + b)

parse = \str ->
    str
    |> Str.split "\n"
    |> getPair
    |> List.map pairToNum

view this post on Zulip Eli Dowling (Jan 04 2024 at 00:38):

I have a whole other chunk of code that implements a simple "wordfinder" that just steps along looking for matches for a specific string and keeps track of any partial matches as it goes.
but i need to iterate through both forward and backwards, so the words I'm matching and the the output needs to be reversed

view this post on Zulip Richard Feldman (Jan 04 2024 at 01:36):

ah! So considering ifF is always passed List.reverse, personally I'd write this function:

reverseIf : List a, Bool -> List a
reverseIf = \list, shouldReverse ->
    if shouldReverse then list else List.reverse list

view this post on Zulip Richard Feldman (Jan 04 2024 at 01:37):

then the two call sites would look like this:

name
|> Str.toUtf8
|> reverseIf rev
matchNum = match |> reverseIf rev |> Str.fromUtf8 |> Result.withDefault ("") |> nameToNum

view this post on Zulip Richard Feldman (Jan 04 2024 at 01:39):

I like how self-descriptive |> reverseIf rev is :smiley:

view this post on Zulip Eli Dowling (Jan 04 2024 at 01:47):

That's fair, I suppose in almost any case you would want an Id function you could probably make a more descriptive wrapper.

I will take a look through some of my other functional code and see where I've used the "Id" function to see if there are any other use cases that would warrant it more.

I do think mostly it would be cases where you are returning one of a few different transformation functions and you sometimes want to just do nothing

view this post on Zulip Elias Mulhall (Jan 04 2024 at 02:02):

I've used Result.keepOks \a -> a pretty frequently

view this post on Zulip Brendan Hansknecht (Jan 04 2024 at 02:11):

That feels like a case of maybe bad naming on our part. Cause it really should be List.mapAndKeepOks with a separate List.keepOks

view this post on Zulip Eli Dowling (Jan 04 2024 at 02:17):

As a note on that, I'm a big fan of using the name List.choose for that functionality

view this post on Zulip Eli Dowling (Jan 04 2024 at 02:21):

I guess I certainly think that mapAndKeep crosses a line of verbosity

view this post on Zulip Brendan Hansknecht (Jan 04 2024 at 02:23):

Yeah, I don't think mapAndKeep is a good name it is just being honest about functionality. I think a more common name is filterMap, right?

view this post on Zulip Richard Feldman (Jan 04 2024 at 02:25):

yeah it's basically filterMap except it seemed weird to have one for Ok but not Err

view this post on Zulip Richard Feldman (Jan 04 2024 at 02:25):

since we have keepIf and dropIf

view this post on Zulip Richard Feldman (Jan 04 2024 at 02:26):

also for autocomplete discoverability there's an argument for having it start with List.map____

view this post on Zulip Richard Feldman (Jan 04 2024 at 02:26):

so if you start typing that it comes up in autocomplete

view this post on Zulip Richard Feldman (Jan 04 2024 at 02:26):

a lot of Roc function names are designed with autocomplete discoverability in mind

view this post on Zulip Brendan Hansknecht (Jan 04 2024 at 02:28):

Is performance the only reason not to have it as a separate List.map |> List.keepOks with keepOks being literal and essentially defaulting to the identity version?

Long term if we had automatic under the hood iterators would that fix the perf as well

view this post on Zulip Eli Dowling (Jan 04 2024 at 02:28):

That's pretty compelling tbh.
Maybe just "mapKeep"

view this post on Zulip Eli Dowling (Jan 04 2024 at 02:32):

Would it be possible to go fully generic and make a function like List.keep Ok mylist ?

view this post on Zulip Eli Dowling (Jan 04 2024 at 02:32):

That only keeps items with that tag and returns the tag's content?

view this post on Zulip Eli Dowling (Jan 04 2024 at 02:37):

See a pattern that's come up a few times for me is

mylist= [TagA 1, TagB "hi", ..etc]
mylist|> List.KeepOks \ a ->
  when a is
     TagA a -> Ok a
      _-> Err {}

And that fuction would make it way easier
(Wrote this on mobile sorry if it's not quite right :sweat_smile:)

view this post on Zulip Brendan Hansknecht (Jan 04 2024 at 03:10):

Something like that should work, but only if that tag contains only a single field

view this post on Zulip Brendan Hansknecht (Jan 04 2024 at 03:11):

Hmm....though you can't pattern match on variables, so nvm

view this post on Zulip Eli Dowling (Jan 04 2024 at 03:13):

Yeah that was my thought, you guys don't have some "magic compiler functions" that would let you check if a variable is a specific tag and then dump out the contents of that tag?

view this post on Zulip Eli Dowling (Jan 04 2024 at 03:14):

This is probably a place where you either need reflection, some kind of inbuilt trickery, or macros

view this post on Zulip Richard Feldman (Jan 04 2024 at 03:15):

might not feel ergonomic, but one idea would be:

List.keepMap Result.isOk \elem -> ...
List.dropMap Result.isOk \elem -> ...

view this post on Zulip Richard Feldman (Jan 04 2024 at 03:16):

eh, but then you end up with all the Ok wrappers still there

view this post on Zulip Eli Dowling (Jan 04 2024 at 03:17):

Or you have to write custom ismytag functions for other tags

view this post on Zulip Richard Feldman (Jan 04 2024 at 03:17):

Brendan Hansknecht said:

Is performance the only reason not to have it as a separate List.map |> List.keepOks with keepOks being literal and essentially defaulting to the identity version?
Long term if we had automatic under the hood iterators would that fix the perf as well

perf is one reason, but also it can be convenient when doing things like

List.filterMap strings Num.fromStr

view this post on Zulip Brendan Hansknecht (Jan 04 2024 at 03:43):

Ah, for sure

view this post on Zulip Brendan Hansknecht (Jan 04 2024 at 16:00):

Though if we had the working automatic iterators (again thinking long term) that would be equivalent to:

List.map strings Num.fromStr |> List.keepOks

Not much more verbose and theoretically would be same perf.

view this post on Zulip Richard Feldman (Jan 04 2024 at 16:49):

true!


Last updated: Jul 06 2025 at 12:14 UTC