Fork me on GitHub
#clojure-uk
<
2021-03-09
>
dharrigan07:03:21

Hello Peeps!

danm13:03:01

I am having a total macro fail. I have a ns, x, in which I've got a macro defined

(defmacro gauge
  ([metric value]
   `(statsd/gauge ~(format-metric metric ".value") ~value (context))))
format-metric is a defn. If I call the macro as-is with a string value for metric (e.g. (x/gauge "foo" 1)) it works fine. If I call it with a function that will return a string, e.g. (x/gauge (name :foo) 1) I get an exception because (name :foo) is not a string. This is all fine, I understand why it's happening. I can (macroexpand (x/gauge ~(name :foo) 1)) and get what I want, but that requires knowledge it is a macro and of macro escape chars in the caller. I can't for the life of me work out what sequence of macro escape characters I need in the defmacro` to allow metric to work as either a bare string or a function that will resolve to a string without the caller having to do anything special

mccraigmccraig13:03:04

@carr0t does (statsd/gauge (format-metric metric ".value") value (context))))` not do what you want ?

danm13:03:19

facepalm You know, I think it might. ☝️:skin-tone-2: is what was there already, with the ~ on the entire format-metric call, and it didn't occur to me I'd need to get rid of that for a ~ on metric (which I had already tried) to work

mccraigmccraig13:03:55

yw - and if you aren't already using it, CIDER's macroexpand is my favourite tool for helping me to bludgeon macros into submission - point it at any piece of code and macroexpand away

alexlynham13:03:11

at the risk of being a scumbag, does this require a macro? i can't visually parse what's going on here but it feels like a normal HOF would work &/or failing that a partial application

danm13:03:40

format-metric inserts the namespace, so that we always know where it was emitted from. That doesn't work (without manually providing it) unless it's a macro

alexlynham13:03:51

namespace of the calling location or definition location?

alexlynham13:03:01

(i assume the former)

danm13:03:52

The former

alexlynham13:03:13

i'd never have thought to write a macro for that, if i wanted to know more info i think i'd have parameterised it, but then i think i'm relatively macro-averse for a lisper heh

alexlynham13:03:27

i guess it's the implicitness that i don't like when it comes to a lot of metaprogramming (thinking of ruby perhaps more than clojure actually for some of the worst offending code i've seen)

Jordan Robinson13:03:33

hot take time: I think it's good to be macro-averse

mccraigmccraig13:03:09

the first rule of macroclub is real - but i have written fns which take explicit "location" params, alongside a macro which expands to that fn with the "location" param filled in

danm13:03:19

Agreed. They're often more trouble than they're worth. But in this case I think it's useful

danm14:03:34

We've got various calls to the same function we want metrics on all through the codebase, and we want to be able to tell where that call originated. Rather than wrapping every one in an independent metric with params etc, we use this around the function itself, in another macro. Then all our calls are to that macro, and they all get unique metrics recorded based on their namespace

alexlynham14:03:40

me, coming to after too long working with monadic pipelines

alexlynham14:03:43

> "Come back, so we can be young men write macros together again"

mccraigmccraig14:03:18

macros go great with your monadic pipelines @alex.lynham, you've just been working in a language which doesn't have macros, so you've gotten all stockholmed to doing without 😬

alexlynham14:03:09

i think that a lot of the problems that we've had in prod have been at runtime so i guess i'm now starting to think i want compile time to be even more aggressive is what it is

alexlynham14:03:11

also yeah, a period of strong static typing has had me thinking a bit differently about issues i've had in the past where maybe the root cause was dynamic typing (if you get all the way down to it :thinking_face:)

mccraigmccraig14:03:38

i would dearly like some static typing sauce on my dynlangs

jiriknesl14:03:03

I like gradual typing a lot. But adding it to the language not designed to be statically typed can be tricky (Typed Clojure).

alexlynham14:03:38

maybe in my heart of hearts i want carp with a good standard library, idk

jiriknesl14:03:57

What do you use monadic pipelines for?

jiriknesl14:03:43

We use it for our controllers & middlewares/interceptors, where they are basically AppState -> Either AppState shaped and it gives us stateless controllers and middlewares while retaining nice “Developer Experience”.

alexlynham14:03:59

we're in typescript, writing serverless/aws lambdas

mccraigmccraig14:03:25

i've looked at carp a few times and lux more than a few times 🙂

alexlynham14:03:52

so all our handers init a system state, then just call a fold action that resolves a big 'ol chain of deferred Either monads down to either a success or error HTTP response which we return

jiriknesl14:03:54

Monadic pipelines in TS?

mccraigmccraig14:03:22

totally agree @jiriknesl - if static checking isn't there from the start then it's probably going to be quite api-breaking

alexlynham14:03:28

then every single entity basically just plugs together the database lookups and all the transformations required to get from HTTP request -> HTTP response and it's all on rails

mccraigmccraig14:03:16

we're using fairly basic monadic pipelines in clj in our api and routing

jiriknesl14:03:16

> required to get from HTTP request -> HTTP response and it’s all on rails This is exactly what we want to achieve. Stateless applifecycle that will give convenient methods you could have in Rails/Django, but without any hidden state.

alexlynham14:03:49

so our handlers look a bit like

export const usersPublicGet = async (event: requests.MinimalRequestEvent): Promise<responses.Response> => {
  const system: db.System = await db.getSystem();
  const client = system.pgclient;

  try {

    const response = await p.pipeline(event, up.getPublicPipeline)(system)();
    return response;

  } catch (err) {
    console.log(err);
    return responses.respond500('http/error', 'Something went wrong');
  } finally {
    await db.closeDbConn(client);
  }
}; 

alexlynham14:03:05

where the const response line is where the magic happens

alexlynham14:03:27

and then a pipeline is just lots of ReaderTaskEithers where the System is in the Reader and the TaskEither is just a deferred/async Either

const getPublicPipeline = (event: requests.MinimalRequestEvent): RTE.ReaderTaskEither<db.System, t.ErrorType, t.EntityType> =>
  Do(RTE.readerTaskEither)
    .bind('userId', RTE.fromEither(requests.getIdFromPath(event)))
    .bindL('getUserRes', ({ userId }) => uDb.getUserRTE(userId))
    .bindL('user', ({ getUserRes }) => RTE.fromEither(uXfms.getUserFromResE(getUserRes)))
    .bindL('userNoPII', ({ user }) => RTE.fromEither(uXfms.userToShareableE(user)))
    .bindL('getUserFileRes', ({ user }) => fDb.getFileRTE(user.file_uuid))
    .bindL('userFile', ({ getUserFileRes }) => RTE.right(fXfms.getUserFileDataFromRes(getUserFileRes)))
    .bindL('userNoPIIWithFile', ({ userNoPII, userFile }) => RTE.right(R.mergeRight(userNoPII, { file: userFile })))
    .return(({ userNoPIIWithFile }) => { return { entityType: t.EntityTypes.user, entity: userNoPIIWithFile }; });

mccraigmccraig14:03:44

i do like the destructuring approach to binding variables - it's a neat workaround

alexlynham14:03:40

it's less terse but very explicit about what's going on

alexlynham14:03:46

the gotcha is that if you don't deref the var in a bind (i.e. you use a bind not a bindL in a step that's later), you sometimes see async things go awry, but i guess i expected that cos of how mlet works, so i only realised it could cause a bug recently

alexlynham14:03:18

i think that's why most people just use pipe and chain from the fp-ts core lib

mccraigmccraig15:03:35

orly - i was getting the impression that bindL was semantically bind, but supplying a map of all the in-scope bound variables to the function, rather than just the last bound variable ?

alexlynham15:03:55

iirc there's a gotcha

alexlynham15:03:15

oh actually i think it might be to do with the ordering of do vs doL

alexlynham15:03:35

i think do can execute earlier than it's listed in a chain if it doesn't ref the context

mccraigmccraig15:03:19

ohhhh, is it actually an applicative-do, rather than a monadic-do ?

alexlynham15:03:40

well it seems to be monadic when the context is in scope, so possibly it's just a bug

alexlynham15:03:00

it was just one time where something v weird happened so i needed to add a binding

alexlynham15:03:16

although maybe i'm misremembering

alexlynham15:03:20

:woman-shrugging:

mccraigmccraig15:03:05

it would probably look the same... applicative-do looks, and often behaves, like a regular monadic do (https://gitlab.haskell.org/ghc/ghc/-/wikis/applicative-do) but steps can happen concurrently

alexlynham16:03:59

hmmm, there's a specific sequence form (i think that's the api i'm thinking of) to do that style of parallel/applicative stuff, so maybe i'm just remembering wrong