Trying to avoid imperative code

The code you wrote seems like a middle ground between the crude “case stack”, and my current dictionary solution here.

But it still has the problem pointed out by @kindaro. If you accidentally mix up some do_A1 with do_B2 because of typo, nothing will catch the mistake.

But it still has the problem pointed out by @kindaro. If you accidentally mix up some do_A1 with do_B2 because of typo, nothing will catch the mistake.

Perhaps, but that code is essentially “library” code. It’s written once and is always correct, or not, for any client that calls it. I don’t think that trying to obtain additional type safety in this particular corner of your design is going to end successfully.

Perhaps it would be sensible to approach this from a slightly different angle.

data Entry = Gmail | Facebook | YouTube deriving (Enum,Bounded,Eq)
 -- All of the entries as a list.
allEntries :: [Entry]
allEntries = [minBound..maxBound]

-- The compiler will warn us if we omit any entries here!
renderEditEntry :: Entry -> IO ()
renderEditEntry Gmail = [...]
renderEditEntry Facebook = [...]
renderEditEntry YouTube = [...]

-- The compiler will warn us if we omit any entries here!
renderEntry :: Entry -> IO ()
renderEntry Gmail = [...]
renderEntry Facebook = [...]
renderEntry Youtube = [...]

-- Since the activeEntry is passed in as a singular entity, it ensures that only one of them is active.
-- Generating and storing that "active" entry can be done however you like - this function doesn't care.
renderEntries :: Maybe Entry -> IO ()
renderEntries Nothing = forM allEntries renderEntry
renderEntries (Just activeEntry) = forM allEntries
    $ \e -> if e == activeEntry then renderEditEntry e else renderEntry e

apologies if this is rendered poorly, I’ve not used Discourse before!

In the above, we firstly separate out the definitions of renderEntry and renderEditEntry. (If they are very similar, it would be possible to further abstract out some common aspects!)

We then write a very short renderEntries function which goes through all elements of the Entry datatype and spits out a text entry or input field for each, depending on whether the activeEntry field matches the value of the “current” one.

Yeah, I guess. That’s a possible conclusion, that it’s unnecessary to go too far trying to improve this code.

I’m starting to think that functions with pattern matching is the way to go. I guess it’s a misstep when I wrote entirely unrelated functions like drawActivityField, drawServiceField, etc. Although I’ll probably encounter other problems when I try to apply the strategy.