[HN Gopher] Code Smell of the Day: Type Keys (2021)
___________________________________________________________________
 
Code Smell of the Day: Type Keys (2021)
 
Author : genericlemon24
Score  : 105 points
Date   : 2023-01-02 11:25 UTC (11 hours ago)
 
web link (jesseduffield.com)
w3m dump (jesseduffield.com)
 
| _3u10 wrote:
| This feels like inheritance to me, AdminUser inherits from User
| both have a setup / create method... Or just use a constructor...
| 
| For the most part I feel whatever gets the job done in the least
| lines of code is usually the right solution.
| 
| But overall this entire exercise smells suspiciously like a
| constructor.
 
  | code_runner wrote:
  | Yeah I assumed we were going to get to some sort of
  | types/inheritance based solution as well.
  | 
  | Stuff like this doesn't bother me a ton unless it's littered
  | everywhere. If every function can run one of two paths that's
  | certainly annoying... if there is some high level function that
  | builds one of two "pipelines" and I don't think about userType
  | again, I don't care too much.
 
| yongjik wrote:
| The original version isn't a code smell. It has a very important
| advantage: read the function and you know that a user can be only
| one of the two things, a "customer" or an "admin", and you never
| have to worry about any other cases, because they don't exist.
| 
| Make it a callback, and now the code becomes "Here we do
| something specific to each subclass of users, but this code
| doesn't tell you what they are, there can be any number of
| classes and the callback can literally do _anything_ , and if you
| want to figure out what's happening at this particular step,
| scour the entire codebase and find all the callbacks that are
| passed as arguments."
| 
| The code has become extensible, and thus harder to reason about.
| This is a good trade-off if you _know_ you will have more and
| more user roles, added by different devs (or even different
| teams), and you want the user creation code to be decoupled from
| roles.
| 
| On the other hand, if most of your business logic is "Do this for
| customers" and "Do that for admins," then it's a useless
| extensibility. You will need a major change of your business
| requirement before a callback would be useful.
| 
| YAGNI.
 
  | rcme wrote:
  | "Do this for customers" and "do this for admins" has to be the
  | number one regret of people who implement systems requiring
  | access controls. Using roles and privileges is a much better
  | approach.
 
| Octokiddie wrote:
| Some are suggesting that the author is against the use of tagged
| unions.
| 
| I don't read it that way. What the author is against is using
| parameters for the specific purpose of controlling the flow of a
| function. Instead, the author recommends breaking the function up
| into as many functions as there are variants of the type key.
| 
| This also applies to boolean flags (where only two variants are
| possible). There are, of course, exceptions as the author points
| out.
| 
| The small win is that the type key no longer needs to be
| maintained. The big win is that each function that gets split out
| has fewer reasons to change. The individual functions are
| therefore easier to maintain and understand.
 
  | dragonwriter wrote:
  | > Some are suggesting that the author is against the use of
  | tagged unions.
  | 
  | > I don't read it that way. What the author is against is using
  | parameters for the specific purpose of controlling the flow of
  | a function. Instead, the author recommends breaking the
  | function up into as many functions as there are variants of the
  | type key.
  | 
  | Sure. How do you call the right one of the combined set of
  | functions? A conditional branching on the type key at the call
  | site? But then, by the same rule, the _calling_ function should
  | be split into multiple functions instead, and that recursisvely
  | happens until you reach each point an instance of the typed
  | union that _might_ , ever, be passed (indirectly) to the
  | function at issue is defined. And then you have a bunch of sets
  | of near-identical functions where every function in the set
  | needs updated for any required change that _doesn't_ depend on
  | the tagged union. And the tagged union is no longer really
  | serving the purpose of a tagged union, which is exactly to
  | allow _not_ having that kind of duplication.
 
    | Fellshard wrote:
    | I think the idea is that it's better to maintain several
    | functions whose implementations are each linear flow than to
    | maintain a single function that is littered with type key
    | checks to turn on and off parts of its flow. It's trading off
    | comprehension of each function by adding some risk that those
    | functions will have shared parts that go out of sync.
 
    | [deleted]
 
  | motogpjimbo wrote:
  | > The big win is that each function that gets split out has
  | fewer reasons to change. The individual functions are therefore
  | easier to maintain and understand.
  | 
  | Yet the namespace becomes more polluted and the
  | interconnections between the functions becomes more
  | complicated, particularly during debugging.
  | 
  | It's important to remember that "break big things into smaller
  | things" only pushes complexity someone else rather than
  | eliminating it altogether, while simultaneously increasing the
  | overall complexity of the system by increasing the number of
  | interconnections between the parts. That doesn't mean you
  | shouldn't do it but you should be honest about the
  | consequences.
 
| thdc wrote:
| If possible, I would prefer deriving the "userType" from
| UserAttributes.
| 
| I see various methods for doing so are covered in the follow-up
| post linked at the top of tfa.
| 
| This is also avoidable if you have proper class/interface
| hierarchies, where the type "field/key" is the type itself.
| 
| In the case it's not possible and there really is only 1 type
| that is fully applicable to both admins and normal users then I
| would probably be fine with either approach (although I'd
| consider updating the return type to get type safety between
| admin and normal users!)
 
| dfee wrote:
| The author is against tagged unions. Ok.
| 
| https://en.m.wikipedia.org/wiki/Tagged_union
 
  | jjice wrote:
  | The article felt like it was more against them being used in
  | functions as switches. I don't think (could have missed it)
  | that there was any discussion about using them in transport,
  | because they are necessary there, but deserializing them to
  | richer objects before going into code like was detailed in the
  | article is a good idea IMO.
 
    | Jtsummers wrote:
    | The article actually specifies that they are known
    | statically, and not actually dynamic values. They are _not_
    | coming from databases or other external sources and that 's a
    | large part of why it's a code smell. From the article, near
    | the top but below where many people in this thread stopped
    | reading:
    | 
    | > its value is always known at compile time. We're not
    | receiving it as an parameter from an HTTP request or a value
    | from the database.
 
| fovc wrote:
| A common variant of this is the Boolean argument to flip between
| function modes                   doAOrB(args, aOrB: bool)
| 
| Especially if the toggle gets used in 3-4 places or more in the
| body, control flow is a nightmare to follow. Instead of 2 paths
| to read, you have to mentally coalesce 2^n paths into 2. Worst-
| case scenario is when args has different meanings depending on
| the value of aOrB.                   @param flag: if set and if
| aOrB is true, warnings log to stdout and die, otherwise warnings
| are saved in the foobar field of args
 
  | meindnoch wrote:
  | My favorite version is the "forceX" antipattern, where you
  | introduce boolean flags to turn off some aspect of a function.
  | E.g. "loadData(forceFromDisk: bool)", where "forceFromDisk ==
  | true" bypasses the cache.
  | 
  | Then somewhere down the line someone realizes that actually in
  | some cases of "forceFromDisk == true", we _still_ want to load
  | the data from the cache, so the function becomes
  | "loadData(forceFromDisk: bool, forceCache: bool)" where
  | "forceCache == true" overrides "forceFromDisk".
  | 
  | Then somewhere down the line someone realizes that actually in
  | some cases of "forceCache == true", we still want to load from
  | the disk... Repeat ad infinitum.
 
    | gherkinnn wrote:
    | Two or more coupled bools are crying to become a union. A
    | function who's control flow needs to be configured cries to
    | be split up.
 
  | [deleted]
 
  | jameshart wrote:
  | My golden rules of programming:
  | 
  | * All booleans really want to be enums
  | 
  | * All enums really want to be rich data objects
  | 
  | * All rich data objects really want to be discriminated unions
  | 
  | That is, whenever you write some code which accepts a Boolean
  | parameter that drives its behavior, at some point you will
  | regret making it a bool and inevitably need to refactor it to
  | be an enumeration type with more than two values. But then
  | eventually you will realize that you have one behavior in your
  | code that applies to more than one of those enum values (all
  | the 'type a' cases but not the 'type b' cases), and you will
  | want those enum values to themselves have a Boolean property
  | telling you whether they are of type a or type b (and note that
  | that Boolean will also be subject to this same golden rule in
  | time)
  | 
  | Eventually you'll find that those different enum values (the
  | type a and type b ones) need different sets of dependent data
  | (type a values all have a 'target' as well, but type b ones
  | don't, say) - so you end up needing a discriminated union to
  | capture the various data objects involved.
  | 
  | All of which leads us to: every if statement and switch
  | statement (over a parameter or input value) is a disguised
  | match on a discriminated union type. Over time, this fate is
  | inevitable.
  | 
  | And if your language doesn't have discriminated unions, learn a
  | pattern to fake them (usually it's the abstract factory
  | pattern).
 
    | jameshart wrote:
    | Also: forgot the evolution into its final form, which is a
    | list of rule objects.
    | 
    | A practical example:
    | 
    | Your program starts out accepting a config file that looks
    | like this:                   enableLogging: true
    | 
    | Then it changes into:                   logLevel: WARN
    | 
    | Then that becomes:                   logging: {
    | level: WARN             file: system.log         }
    | 
    | Then:                   logging:              fileLogger: {
    | level: WARN                 file: system.log             }
    | 
    | And finally:                   logging:              -
    | fileLogger: {                 level: WARN
    | file: system.log             }             - consoleLogger: {
    | level: DEBUG             }
 
      | i_am_toaster wrote:
      | The truth of this comment leaves me feeling somewhat
      | emotionally violated.
 
    | theteapot wrote:
    | Question is whether and to what extent you can shortcut that
    | lifecycle, and whether it's worth trying.
 
      | jameshart wrote:
      | Right. I tend to think you should just be aware of it, be
      | deliberate in choosing where to situate your code on it,
      | and be adept at refactoring code up the scale.
 
    | d3nj4l wrote:
    | I've seen the general idea here called "part blindness"
    | before: https://www.hxa.name/notes/note-
    | hxa7241-20131124T0927Z.html
 
    | toolslive wrote:
    | It's just the second law of Thermodynamics at work, no ?
 
    | culi wrote:
    | I think this evolution paradigm is useful to keep in mind
    | when designing the API, but obviously each successive step is
    | more computationally expensive and more complex (see: error-
    | prone)
    | 
    | You should probably generally use the least advanced of these
    | while maintaining a pathway for future refactors into more
    | complex versions of this
 
| player2121 wrote:
| This code is easy to read and understand and refactoring it with
| a callback seems as premature optimization which does bring own
| flavor to it. In other words, this is a good example when perfect
| is the enemy of the good.
 
| cbrogrammer wrote:
| > const createUser = (
| 
| > attributes: UserAttributes,
| 
| > onCreate: (user: User) => void
| 
| > ): User => {
| 
| > user = User.create(attributes);
| 
| > onCreate(user);
| 
| This stood out to me, in an otherwise tame article, as probably
| the most terrible overthought interface possible. A literal
| callback function passed as a function reference to something
| that does three lines of code? Good luck with that, I much prefer
| even a slightly procedual implementation.
 
  | rglullis wrote:
  | The three line function here is an example, but I spent a good
  | part of a sprint having to deal with a 730-line function that
  | could've been used as a perfect "real world scenario" of why
  | this abuse of control flags (or type keys) is a bad idea.
 
| IshKebab wrote:
| Maybe, but the final solution is definitely harder to follow
| because the function definition now doesn't contain all the
| possible code that can run. You have to "find all references" and
| read the code of all the callers to know what it might do.
| 
| It's more loosely coupled and that always makes code harder to
| follow. So I'd say it could go either way. Depends on the
| specific example.
 
| gpjanik wrote:
| This entire article assumes that the type is only used to satisfy
| what's on display in the example function, which is almost never
| true.
| 
| The article itself is some sort of a programming flex I'd fire
| you for if you leave it in the code reviews too often. :P
 
  | blowski wrote:
  | I really dislike the trend for saying "I'd fire you for x"
  | where x is merely a suboptimal choice, or even a harmless
  | decision about which the author has strong opinions. Probably
  | it is said in jest but it creates a hostile atmosphere for
  | juniors, where they are worried they can be fired for using the
  | wrong design pattern.
 
  | sneak wrote:
  | I envy your access to your recruiting organization.
 
| nonethewiser wrote:
| This has a name. It's called "control coupling." Like the
| author's example, passing in a flag to a function that controls
| what the function does is control coupling and you should prefer
| different functions for each case instead.
 
  | klyrs wrote:
  | > you should prefer different functions for each case instead.
  | 
  | No I shouldn't. I use this frequently in c++ to deduplicate
  | common functionality that only differs by a single line. The
  | "flag" is a zero-size struct and checking the flag is done at
  | compile time.                 struct do_bar_tag {}
  | template       int foo(int x, do_bar_t) {
  | if constexpr (is_same::value) {
  | bar(x);         }         ...       }
  | 
  | The compiler will emit two implementations, as you suggest is
  | proper, but my maintenance overhead is reduced. And the cost is
  | zero, in time and space, at runtime.
  | 
  | Of course, this isn't _always_ the right thing to do, and you
  | can definitely overdo it. But in moderation, it 's fine. "Code
  | smell" is too often a needlessly strong opinion.
 
    | tacotacotaco wrote:
    | Just because you do it frequently doesn't necessarily make it
    | good. Your approach increases the function's cyclomatic
    | complexity. The struct requires the caller to dictate the
    | function's inner logic instead of the function logic being
    | driven by application data. Maybe your solution is a good fit
    | in your production code. I can't say. Maybe there is a
    | different approach to solve your boilerplate concerns. There
    | will be exceptions but generally I don't agree with the
    | example as provided.
 
    | snarfy wrote:
    | It never stays that way. It starts as
    | foo(int x, do_bar_t)
    | 
    | With time, it turns into                   foo(int x, int y,
    | do_bar_t)
    | 
    | and eventually                   foo(int x, int y, int z, int
    | w, do_bar_t)
    | 
    | It's too tempting to add 'just one more' flag. You de-
    | duplicate common functionality but increase overall
    | complexity.
 
      | mycall wrote:
      | Put the flags into a class and pass the class around.
 
        | 0xCMP wrote:
        | s/class/typed object/ -> "How to write Typescript"
 
      | gpderetta wrote:
      | And the issue with too many behavioural flags is that now
      | the function (and the implementor) has to deal to all
      | possible combinations, even though by specs only a small
      | subset would be allowed.
 
      | klyrs wrote:
      | Only, I don't use ints as "flags," so you're doing it
      | wrong. And you're using the slippery slope fallacy,
      | assuming that I'm a DRY absolutist. I'm not. Like I said,
      | it's not _always_ a good thing, and you can definitely
      | overdo it.
      | 
      | The valid complaint about the implementation I show would
      | be that the do_bar logic is at the very top with no
      | preamble, and it may be simpler for the caller to simply
      | call bar(x) in that case. But adding necessary-looking
      | complexity would detract from the clarity of the
      | implementation (and damn c++'s verbosity, it already looks
      | like hot garbage IMHO)
 
        | kwhitefoot wrote:
        | > Only, I don't use ints as "flags," so you're doing it
        | wrong. And you're using the slippery slope fallacy,
        | assuming that I'm a DRY absolutist. I'm not.
        | 
        | You might not "use ints as flags" but a lot of
        | maintenance programmers who know nothing about your idea,
        | have never seen the code before, don't know how the
        | application works or even what it does, will do just
        | that.
 
        | klyrs wrote:
        | What you're describing is multiple policy failures. Why
        | are these "maintenance programmers" implementing novel
        | features? Why are they novices that are unfamiliar with
        | the language and its best practices? Why are these
        | novice-rogue-developer-maintainers' contributions not
        | being reviewed by more competent developers?
        | 
        | Moreover, if these unrestricted-novice-rogue-developer-
        | maintainer-rhinoceri are going to do whatever the hell
        | they feel like, completely disregarding the patterns and
        | practices that I have established in my code, _does it
        | matter what I have done?_
 
        | xupybd wrote:
        | Because novice programmers are cheaper and businesses
        | make decisions based on short term cost. Especially when
        | projects get old.
 
        | klyrs wrote:
        | I know all that. But you didn't address my point: if
        | management has thrown all good practices to the wind, how
        | am _I_ to blame for the horrendous outcomes?
 
        | nomel wrote:
        | This is a tangent, but I've never seen the real world
        | work this way. You're being paid to be the DRI for the
        | code base. You _will_ be blamed because you're being paid
        | for it to be your responsibility. Actually horrendous
        | outcomes, which can only be related to breaking
        | functionality rather than code prettiness, can be
        | prevented.
 
        | [deleted]
 
        | xupybd wrote:
        | I'm not blaming you I'm saying most of the time
        | management will pick the cheapest option. That leads to
        | bad outcomes for a lot of legacy software. That then
        | leads to someone spending more money replacing the legacy
        | system
 
      | mixedCase wrote:
      | Part of the job is to know when to call it quits and
      | refactor, there's no hard rule. In most cases I'll take a
      | 130 line procedure with three flags that don't heavily
      | interact with each other over three almost-identical 100
      | line functions.
 
        | culi wrote:
        | Porque No los Dos?
        | 
        | In languages with good support for functional features,
        | you can curry and compose functions to your heart's
        | content, allowing you to reuse similar logic and take
        | better advantage of type systems like TypeScript's
 
        | valand wrote:
        | It comes down to how deep the type system of a language
        | support this kind of operation. It'll work well with TS,
        | Rust, but it doesn't add much to golang, C++.
        | 
        | But well, usually dynamicizing statically-known types
        | yields premature abstraction
 
        | culi wrote:
        | I would actually call it "atomization" more than
        | abstraction, but I agree it can often be premature
 
    | semicolon_storm wrote:
    | For most modern programming problems that we get paid to
    | solve, a bit of duplicate or similar looking code is the
    | least of our problems. Conceptually integrity and managing
    | dependencies/coupling is far more important.
 
      | klyrs wrote:
      | Who is "we" and do you have statistics behind that "most?"
      | And why is "paid to solve" relevant to the discussion? It's
      | okay to say that what I'm saying about my personal
      | experience doesn't resonate with your personal experience,
      | but hand-waving at a nebulous authority isn't making a
      | case.
      | 
      | I use compile-time flags to deduplicate logic because
      | refactoring is a nightmare if tens to hundreds of lines of
      | code are turned into boilerplate. That is to say, I ensure
      | conceptual integrity by maintaining a single source of
      | truth. The nice thing about relatively dry code is that if
      | you need to decouple it, you can easily just duplicate the
      | code in question and run with that. If you have a dozen
      | slightly-different copies of the same boilerplate, it's
      | very rare in my experience for the variations to be well
      | documented, so it ends up taking a lot of work to figure
      | out why the variations exist -- and the real pitfall that
      | I've seen time and time again is that a bug is found in one
      | variant, fixed there, and not propagated to the other
      | variants.
 
        | rcarr wrote:
        | > Who is "we" and do you have statistics behind that
        | "most?"
        | 
        | I love that you've called this out. So many people try
        | and justify their coding preferences by referring to this
        | mythical 'we' or the 'coding community' who have
        | apparently all agreed to to some certain standard and
        | everyone should just fall in line and get behind it
        | without asking any questions. It's such a load of
        | bullshit.
 
  | valand wrote:
  | When I shared this to my colleague, I call it "dynamicizing
  | statically-known info". It is in the same family as putting
  | hardcoded values in a list and calling a .map to it. It almost
  | always end as a premature abstraction.
 
  | aliqot wrote:
  | > This has a name.
  | 
  | Good, I'm glad. For some reason the term "Code smell" has
  | itself become a "smell" to me.
 
| [deleted]
 
| thecodrr wrote:
| Not everything needs to be DRY. In a lot of codebases, I see this
| hell-bent attitude to make everything as DRY as possible often at
| the cost of creating multiple files when all it'd require is 2
| functions.
| 
| A lot of code smells arise from this attitude. DRY is not
| mandatory. Oftentimes, repeating yourself is better and clearer.
| It allows you to grow both functions at their own pace, handle
| edge cases separately, prevent spaghetti code, write better &
| simpler tests.
 
  | sjducb wrote:
  | I 100% agree, but try telling that to a programmer with 2 years
  | experience.
 
    | wizofaus wrote:
    | Even with over 25 years professional experience I still see
    | code where logic and literal constant values (including e.g.
    | complex regular expressions) are duplicated unnecessarily
    | (requiring constant double maintenance etc.) as more of a
    | problem than overly clever ways to reduce repetition that
    | make the code harder to work with. 95% of the time developers
    | can easily save themselves time upfront and in the future by
    | first checking if the logic they're writing already exists in
    | the codebase somewhere and reusing that (usually with very
    | minor refactoring, e.g. marking a function public and moving
    | it into a suitable shared module). Maybe 1% of the time the
    | necessary refactoring is complex enough that it risks
    | introducing bugs or decreasing code comprehensibility and
    | duplicating the logic (ideally with a comment referring to
    | the original source) is the lesser evil.
 
  | deathanatos wrote:
  | > _It allows you to grow both functions at their own pace,
  | handle edge cases separately,_
  | 
  | DRY should only be applied to semantically identical code, not
  | merely syntactically identical code. If the code is in the
  | former category, and should be subject to DRY, and you're
  | making changes to one copy, by definition you must need to make
  | changes to the other copy ... and it's a bug if you're not.
  | 
  | And those are the _worse* kinds of repetition to encounter
  | later on when you 're trying to change the code: I've got two
  | functions, ostensibly doing the same thing ... except not.
  | Which one is correct? (I.e., "What are the requirements?", and
  | in my career, if I'm encountering this situation in the code,
  | the requirements are _never* documented.)
 
| rhdunn wrote:
| Type keys are used in various serialization mechianisms,
| especially JSON/TypeScript APIs (JSON-RPC, RDF-JSON, etc.). Even
| XML serializations have type keys with the element names.
| 
| In these cases, it makes sense to hide those type keys from the
| API and only use them in the serialization/deserialization logic.
 
| kstenerud wrote:
| The problem with these examples is that they're contrived and
| oversimplified. Yes, switched enum behavior can be bad, but it
| can also be the right solution for the time being (or even
| forever).
| 
| The solution in the article could turn out even worse if the
| system evolves to require so many kinds of users that you end up
| with 8 different functions depending on what kind of user you
| want. You can't hide complexity of this type; you can only move
| it around as it emerges and try to keep things understandable.
| 
| In every design situation you're faced with countless approaches
| that you can take, with a lot of uncertainty as to how the system
| will evolve over time. I've found that the "what if" time you
| spend up front follows a parabolic efficiency curve: Early on in
| the design, you throw away some ideas that would make the first
| implementation easy, but would quickly become unwieldly. As the
| design progresses, you start thinking of more and more "what
| ifs", and soon you find your up-front architecture getting
| heavier and heavier, such that if you don't stop with the "what
| ifs", your first implementation will become a cathedral when all
| you'll need for the next year is a priest and a bench.
| 
| Clear out some easy win design issues up front, but overall don't
| sweat it and don't spend much time thinking "what if" because
| you're going to predict the future 50% wrong anyway. Refactoring
| isn't that hard or time intensive except in old code bases with
| poor hygene, and your new project is neither (unless you have
| poor discipline).
 
| martin-adams wrote:
| As a nice side effect of fixing this is that it removed the bug
| where the switch statement didn't have a break statement.
 
  | qayxc wrote:
  | To be fair, every reasonable linter will treat this as an error
  | or at least a warning and inform the programmer about their
  | blunder. In fact, in TypeScript you don't even need a linter -
  | the compiler itself can be configured to disallow fall-through
  | cases. It's smart about it, too and still allows "empty" cases
  | and makes the last case-statement optional, such that "switch
  | (lorem) { case 'foo': case 'bar': ipsum(); break; case '...':
  | blah(); } is still valid and compiles just fine even with
  | noFallthroughCases configured.
 
| Nition wrote:
| > Note that instead of using callbacks we could have subclassed
| User to AdminUser and RegularUser, with each overriding the
| onCreate function.
| 
| Although the pendulum has swung far from inheritance towards
| composition in recent years, this is surely a prime case for
| simply using a subclass and instantiating whichever one you want.
 
| rcarr wrote:
| I'm not sure I agree with this. Surely the solution is not to
| bother with the completely pointless createAdmin and
| createCustomer functions in the first place and to just call
| createUser directly where it is needed with the correct
| parameter. Otherwise you're just repeating yourself.
| 
| Most of the time, DRY is better.
 
| auraham wrote:
| When I started reading the post, I though the author would talk
| about pattern matching to get rid of the switch block. In that
| case, creating a user can be expressed in Elixir as follows:
| def createUser(attributes, "admin") do
| create(attributes) |> setupAdmin |> setupNotifications
| end              def createUser(attributes, "customer") do
| create(attributes) |> setupCustomer |> setupNotifications
| end              # example         User.createUser(%{name:
| "laura", age: 30}, "admin")
| 
| Here, we chain functions using a pipe, |>, assuming that each
| function returns a user-like variable, like %User{}
| 
| I also liked the idea of using a callback for decoupling code
| (although that approach was discouraged by many users in this
| post). It could be done in Elixir as follows:
| def createUserCallback(attributes, callback) do
| create(attributes) |> callback.() |>  setupNotifications
| end              # example
| User.createUserCallback(%{name: "laura", age: 30},
| &User.setupCustomer/1)         User.createUserCallback(%{name:
| "laura", age: 30}, &User.setupAdmin/1)
| 
| Since callback can be any kind of function, we can use pattern
| matching to enforce that the return type of each function is a
| user, ie %User{}:                   def
| createUserCallback(attributes, callback) do             user =
| %User{} = create(attributes)             user = %User{} =
| callback.(user)             user = %User{} =
| setupNotifications(user)             user         end
| 
| Another approach is using a @spec annotation to define the
| signature of the callback.
| 
| Full code:                   defmodule User do
| defstruct name: nil, age: nil, type: nil                defp
| create(_attributes = %{name: name, age: age}) do
| %User{name: name, age: age}           end                defp
| setupNotifications(user = %User{}) do             IO.puts("User
| created #{user.type}: #{user.name}")             user
| end                def setupAdmin(user = %User{}) do
| %User{ user | type: "admin" }           end                def
| setupCustomer(user = %User{}) do             %User{ user | type:
| "customer" }           end                def
| createUser(attributes, "admin") do             create(attributes)
| |> setupAdmin |> setupNotifications           end
| def createUser(attributes, "customer") do
| create(attributes) |> setupCustomer |> setupNotifications
| end                def createUserCallback(attributes, callback)
| do             user = %User{} = create(attributes)
| user = %User{} = callback.(user)             user = %User{} =
| setupNotifications(user)             user           end
| end
 
| swisniewski wrote:
| The author has probably never written a compiler before.
| 
| Inside any compiler, you are going to have "type keys" and switch
| statements all over the place.
| 
| Even if you are writing a compiler in languages with builtin
| "tagged unions" and "pattern matching" you are still using "type
| keys" and switch statements.
| 
| Trying to write a compiler without a switch statement would lead
| to a giant unreadable mess.
 
| tincholio wrote:
| Sounds like he could use a language with multimethods...
 
| SecondRikudo wrote:
| I mean... you already have user.setupNotifications() in your
| example... the obvious solution would have been a polymorphic
| user.setup() which is a different implementation for the
| CustomerUser class and the AdminUser class... if you've already
| picked OO as your paradigm of choice, at least exploit it for its
| value, you've already paid the costs.
 
| pfraze wrote:
| These kinds of edicts don't work because they fail the most
| important rule of programming: do what's simplest and most clear
| for the code you're writing. Your language is going to have
| constructs for this kind of thing (polymorphic inheritance,
| discriminated unions, multi dispatch, etc). Pick the language-
| native construct that works best. In the case of Typescript this
| would arguably be polymorphic inheritance, but if you don't have
| a scenario that benefits in multiple ways from writing classes
| then using them would be overkill.
 
| rosebay wrote:
| I read the blog, was mortified but luckily others have already
| provided feedback in an earlier thread:
| https://news.ycombinator.com/item?id=28325563
| 
| To the author's credit, I loved that he is thinking analytically
| and he did take the feedback positively - there are some good
| ideas in the post.
 
| svnpenn wrote:
| The code smell I see is the omitted semicolons. The pitfalls of
| doing this are well documented. Just don't do it.
 
  | gherkinnn wrote:
  | The commonly cited pitfalls are either strawmen or exceedingly
  | rare. ASI is predictable and reliable. Not once in 5 years of
  | semicolon-less development did it come to bite me or anybody
  | else in a notable manner.
  | 
  | By all means use them in your work, but don't treat it as
  | ultimate truth. It is not.
 
| exelib wrote:
| Not sure I agree with the author. It took me less than 3 seconds
| to understand the initial code, but at least 10 seconds to
| understand the final result, even I had the knowledge of the
| initial function. But in some context this pattern can make a
| perfectly sense. Just not in this example.
| 
| Furthermore, from my point of view, creating an anonymous
| function and then store it in a variable is a bigger code smell.
 
| satvikpendem wrote:
| In a language like Rust or OCaml with exhaustive pattern matching
| on algebraic data types, the initial example is the recommended
| approach, because you will never miss a case, it forces you to
| think through all possible cases, where as the later refactored
| example in the article do not have such a constraint.
 
  | qayxc wrote:
  | The same is true for Typescript, hence the userType's
  | restriction to 'customer' and 'admin'.
  | 
  | Though the initial sample code is wrong anyway - note the
  | missing "break" statements inside the switch.
 
| metadat wrote:
| Previously discussed in August 2021:
| 
| https://news.ycombinator.com/item?id=28325563 (74 comments)
| 
| Credit: Thanks to @rosebay for pointing this out at
| https://news.ycombinator.com/item?id=34218449 (now dead)
| 
| Also see additional interesting submissions from
| jesseduffield.com:
| 
| https://news.ycombinator.com/from?site=jesseduffield.com
 
___________________________________________________________________
(page generated 2023-01-02 23:01 UTC)