-
Notifications
You must be signed in to change notification settings - Fork 307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
clarify how inputs with defaults are implicitly optional #464
Conversation
As a community member, I'm happy with this interpretation, and I think you've done a great job to clarify the subtlety between Optional with default supplied and non-optional with default , especially with the table. |
@illusional thanks! I'm happy to get this clarified too. I'd love to hear from somebody who has poked this specific behavior in Cromwell to see if it's captured correctly here, ie no reason to codify something that requires changes in all engines. cc @cjllanwarne @aednichols @pshapiro4broad or anyone really |
I'm not wild about this because it creates a special case that isn't covered by the type rules. To be specific, I'm referring to the case where you declare a non-optional input but WDL would allow you pass an optional typed expression to it. So what are the alternatives? Making the caller copy defaults up is bad. I can see two possibilities
I don't speak for the cromwell team though, I'm a WDL user not a cromwell developer. |
@mlin I like the table! I think it also helps to provide the explicit reminder that If I understand correctly, the only ambiguity we are discussing here is the case where we are calling a task with a non-optional input and passing a Personally, I do not think this is ambiguous with respect to the rest of the spec. But if we are instead proposing to add a special case here (thus breaking the type system even further than it already is), I am against that. I don't think the convenience afforded by this special case justifies the additional cognitive load for the user, nor the additional burden to the runtime implementer. There are already mechanisms to call a task with a non-optional input using an optional value, and I think they are sufficient:
|
@jdidion the pickle we're in (see the linked prior discussions) is that this
isn't a good general solution because it requires the caller to specify the default value, which shouldn't be its concern. Changing the default value would necessitate changing all the calls. We get to this compromise where we don't want the workflow to have to know the default value, but we do want to allow pass-through of an overriding workflow-level input: workflow w {
input {
String? s_override
}
call t { input: s = s_override }
}
task t {
input {
String s = "some default"
}
...
} If we don't permit this then we need one of the features @pshapiro4broad suggested above. My view is that the loophole proposed here is the least bad, but also that reasonable people can disagree. If a tie-breaker is needed, some weight should be given to evidence that Cromwell permits this and workflows in the wild appear to use it (broadinstitute/gatk-sv#154; but I haven't run that to completely verify expectations) |
I do understand the desire for a work-around. I don't think it's very often the case that a default value changes, or that one person is using another person's "library" of WDL and wants to rely on them to define the default, but when it does happen it's annoying. In my opinion, the better option would be to leverage the special syntax we already created for implicit binding (https://github.com/openwdl/wdl/blob/main/versions/1.1/SPEC.md#call-statement) and put the special case there. For example, workflow foo {
input {
Int? x
}
call bar { input: x }
...
}
task bar {
input {
Int x = 1
}
... If |
I'd def rather find a better future approach, but, I personally just haven't seen the others discussed so far as better -- each one is a compromise. The abbreviated passthrough syntax only applies where the workflow input has exactly the same name.
I have a hard time seeing this POV, may have to agree to disagree on this specifically.. |
Right, that should address the primary use case, which is when my task has a million options and I want to write a workflow that calls it and give the user the ability to override some of those options but let the task define the default values. If there are other compelling use cases we could consider new syntax. Maybe something like |
The way we define In python, the default is only applied when the parameter is ommitted. The following example illustrates this. def foo(bar="biz"):
print(bar)
foo("brat")
=> "brat"
foo()
=> "biz"
foo(bar=None):
=> "" However in our current system, the default value is applied when the parameter is ommitted OR when it is explicitly set to I think we also need to ask ourselves what the user is trying to achieve. Why is the default set so low in the task as opposed to higher up in the workflow? Is this an issue of education and poor practices? Ie a more natural way to write this would be something like the following. workflow foo {
input {
Int x = 1
}
call bar { input: x }
...
}
task bar {
input {
Int x
}
} So, Where does this leave us? For me, I would expect that the implicit option should lead to a type error. IMO A consistent type system will give back to the users of In place its place, we encourage default input values in the workflow foo {
input {
Int x?
}
# new engine function
call bar { input: default(x,1) }
# use select first, with two args, where the second is a default
call bar as bar2 { input: select_first(x,1) }
...
}
task bar {
input {
Int x = 1
}
} |
@patmagee I'm definitely sympathetic to your POV. But I think what @mlin is getting at is that it's a commonly used pattern to have a common task that is called by other workflows, and the task developer may know better than the workflow developer what the default should be for a given parameter. Let's consider the task of BWA MEM, and the specific case of the minimum seed length ( I think it makes sense to provide some way for a workflow developer to say "either override parameter x with the value specified by the user or use the default specified by the task." Furthermore, I think we'd all prefer to do this in a way that doesn't add yet another special case to the type system. Since apparently there is ambiguity in the 1.x specification, and different runtimes currently implement this differently (Cromwell allows passing workflow foo {
input {
Int? x
}
Int? y = if (defined(x)) select_first([x]) * 2 else None
call bar { input: x = y }
...
}
task bar {
input {
Int x
}
} I don't think a function call works here because a function is really just a transformation from an input value of one type to an output value of a potentially different type, and there is no valid transformation from |
Actually, considering further, if we make the semantics of workflow foo {
input {
Int? x
}
Int? y ?= x * 2
call bar { input: x ?= y }
...
}
task bar {
input {
Int x
}
} |
Update: I just spent a little bit of time poking at this with Cromwell 63 and couldn't find a case where it distinguishes Coincidentally, I just had this issue pop up "in real life," so I'm sensing an increasingly urgent need for clarification in some way 😅 |
Wow - talk about the worst of both worlds - allowing To resolve this issue for 1.x, I would vote in favor of your proposed changes if they were instead added to the errata for 1.1 (and we can put them in the main text for 1.1.1) and marked as deprecated. For development/2.0 I think we should come up with a better solution, but we can take that up in a separate discussion. |
Just dug up these interesting historical artifacts: biowdl/tasks@d993500#diff-bb2a20c006384ad4bdd7f90555382acfb7e2be64a99e1d06b6e625ecb509c899R16 So would value input on this topic from @DavyCats @rhpvorderman @aednichols |
As a developer of the BWA MEM task. In this case I would have the default value of One default that we do override is the compression level. Most bioinformatic tools have this at level 5 (htslib) or 6 (gzip), which is plain crazy. It means the tool is spending more time on compressing than on bioinformatic algorithms. So we set it to 1 everywhere. As regards to the issue, Ansible provides an |
@rhpvorderman Are you suggesting to add some method to resort to a task's/workflow's default? Like a function which will cause the input to be omitted if the value given is
|
No a simple keyword
Making omitting explicit with a keyword so to say. Just as with |
I'm open to considering the workflow w {
input {
String? s_override
}
call t { input: s = s_override }
}
task t {
input {
String s = "some default"
}
...
} Would such a call then be supposed to write |
Hmm. Or should that be implicit? s_override is not defined, so omitted by default. |
miniwdl v1.2.0 implements the behavior described in this PR, however, for now (I'm prepared to defend making inputs with defaults implicitly optional -- as a minor relaxation that does what the user wants most of the time, and IMO is not worse than alternatives so far mentioned -- but applying the default when the declared type is optional, is a bridge too far for me currently 😅) |
Given how this could potentially be a contentious topic, I suggest we open this for voting even though it is less a spec change and more a clarification. This behaviour will probably align with what 90% of people expect, and completely contradict what the other 10% would want 😅. @mlin are you happy to transition this to voting? |
@patmagee thanks for labeling this; coincidentally, I also JUST got burned by this ambiguity again "in the wild", so I'm 👍 of course; but I'll also drop a note in slack to get new eyes on this too, since quite some time has passed. |
👍 From me. |
👍 |
bumping this PR once again. It is now currently open for voting |
👍 |
@mlin please change target to |
This creates a loophole in the type system where
None
values are acceptable for an input with a non-optional declared type, as long as it has a default initializer. It's a bit awkward and creates a rather subtle behavioral distinction betweenInt x = 42
andInt? x = 42
, which the proposed table tries to explicate. However, this seems to be the lesser of two evils compared to not having the loophole; specifically when caller wants to pass through their own optional input but let the callee's default apply if it's absent.