HPInc / HP-Digital-Microfluidics

HP Digital Microfluidics Software Platform and Libraries
MIT License
2 stars 0 forks source link

Simpler creation of injectable functions from multi-arg functions #225

Closed EvanKirshenbaum closed 5 months ago

EvanKirshenbaum commented 5 months ago

Thinking about the interaction, a bit, I think that I want the built-ins to return functionals with incomplete arguments, so that you can say, e.g.,

add(w, 5uL of r1)
add(w, 5uL)
w : add(5uL of r1)

That is, if you only provide the liquid or volume, you get a function that takes a pipetting target and returns no value.

Thinking a bit more (and as a prompt for a new issue), it might be useful to have it be a general feature that if you use some distinguishing value for an argument to a function call (say _), you get a function that binds all of the other values and takes a value for that position. That is, add(_, vol) would be return a function that took an argument of the appropriate type and called add with the volume bound as the second parameter.

Originally posted by @EvanKirshenbaum in https://github.com/HPInc/HP-Digital-Microfluidics/issues/224 [comment by @EvanKirshenbaum on Jan 30, 2023 at 11:01 AM PST]

Migrated from internal repository. Originally created by @EvanKirshenbaum on Jan 30, 2023 at 11:02 AM PST. Closed on Feb 09, 2023 at 11:21 AM PST.
EvanKirshenbaum commented 5 months ago

This issue was referenced by the following commits before migration:

EvanKirshenbaum commented 5 months ago

Some alternative approaches would be

  1. Allow the function definer to specify some parameters as injectable. If all but one injectable parameter is provided in a call, what you get is a function that takes that parameter.
  2. Make it so that any parameter is treated as injectable if it can be determined which parameter is missing.
Migrated from internal repository. Originally created by @EvanKirshenbaum on Jan 30, 2023 at 11:06 AM PST.
EvanKirshenbaum commented 5 months ago

The easy part of this was easy. I added at curry_at parameter to the Func.register[...]() functions that takes one or more positions that can be omitted and an AdaptedDelayed/ImmediateCallableValue that takes a function and calls it when invoked. register() registers functions for each curry position that store the argument list and create an AdaptedImmediateCallableValue that inserts their (single) argument into the stored argument list and calls the original function.

This works fine in the simple case. The problem is that I have things like

        fn.register((Type.WELL, Type.LIQUID), Type.WELL, add_liquid_to_well, curry_at=0)
        fn.register((Type.EXTRACTION_POINT, Type.LIQUID), Type.DROP, add_liquid_to_ep, curry_at=0)

If I do that, they each register a form that takes only a LIQUID, and so the second one bumps the first. The obvious solution would be to have a single registration that takes a Type.PIPETTING_TARGET and does the right thing depending on its argument, but that runs into the problem that if you give it a WELL, you should get a WELL (actually, probably NO_VALUE), but if you give it an EXTRACTION_POINT you should get a DROP. Handling this right will require adding the ability in the type system to handle overloading (mentioned in #160) or at least union types, which probably warrants its own issue.

Migrated from internal repository. Originally created by @EvanKirshenbaum on Jan 31, 2023 at 9:00 AM PST.
EvanKirshenbaum commented 5 months ago

Note that there is an earlier issue (#157) on a different approach (using it) as a placeholder.

Migrated from internal repository. Originally created by @EvanKirshenbaum on Feb 07, 2023 at 6:07 PM PST.
EvanKirshenbaum commented 5 months ago

There's a case with transfer in that is a clear PLA violation.

The basic (non-curried) overloads that are defined are

well x liquid -> well
well x liquid x reagent -> well
well x volume -> well
ep x liquid -> drop
ep x volume -> drop
ep x reagent -> drop
ep -> drop

The problem is that when you try to curry transfer in(liquid) or transfer in(volume), you (naively) get either a function that expects a well and returns a well or a function that expects an extraction point and returns a drop. But not a function that can take either. This isn't a problem for transfer out, because both cases can be covered in a single case that takes a pipetting target and returns a liquid. What messes things up here is that in one case we want to return a well and in the other we want to return a drop.

What I'm proposing doing here is to add another mapping on the Func (built-in) that maps curried arguments to options. In the compiler, when we're compiling the injection, I'll compile the left-hand side and squirrel away the type (and maybe the return type to handle compositions?) in the context node for the right-hand side. Then, when compiling a function call whose functor is a built-in, I'll check to see if the context has the annotations. If so, I'll call Func.for_injection() to get a form that takes the injected type into account. If there is no such form, I'll simply use the normal one.

Migrated from internal repository. Originally created by @EvanKirshenbaum on Feb 08, 2023 at 12:19 PM PST.
EvanKirshenbaum commented 5 months ago

That all seems to work. Now, if you say

ep : transfer in(2uL)

you get a drop, and if you say

well : transfer in(2uL)

you get the well. If you just say a bare transfer in(2uL), what you get is a function that takes an extraction point and returns a drop. Oh, well.

The only difficulty was handling the fact that the first form need to get its hand on the environment:

        fn.register((Type.EXTRACTION_POINT,Type.VOLUME), Type.DROP,
                    WithEnvDelayed(lambda env, ep, v: add_liquid_to_ep(ep, Liquid(interactive_reagent(env), v))),
                    curry_at=0)

which means I need to turn the registration of the curried function into a WithEnv form and adjust the curry position to allow for the environment argument.

It also means that the environment that is used in the interactive_reagent() call is the one that was in place when transfer in(vol) was called, but since we're only doing this during an injection, it will necessarily be the same one. If I ever get to the point of creating actually ambiguous overloaded functions that can be passed around and stored, I will need to rethink this.

Migrated from internal repository. Originally created by @EvanKirshenbaum on Feb 09, 2023 at 11:20 AM PST.