Often one parameter is distinguished by whether it could be an array. The Unix cat utility takes multiple sources to a single destination, so analogously:
The problem is, even though it reads smoothly, the names are no longer accurate. The smoothness comes from reading "CopyFile" as a verb (semantically), but "CopyFile" is actually a noun (syntactically).
Source("presentation.md").CopyTo("/tmp/backup")
while scoring slightly fewer slickness points, wins overall imo because it remains accurate.
If we're going to use types to handle this then I'm a fan of the Go method of Writer and Reader. That way, even if you have a method named copy, you cannot really mix the two up. Writer restricts the operations; same as Reader. Having method names imply direction or argument order is gonna be tricky once you add a couple people to your project.
That breaks down when both src and dst implement io.ReadWriter. os.File for example.
I had an adventure upgrading versions of the .NET Core Rabbit library: one of the (boolean) parameters of the Consume function used to be “noAck”, but it got changed to “autoAck” (with inverted behavior). Even though the change was well documented, I would have preferred that to be a breaking change at compile time...
Those are keyword args and are not specific to just Python. :)
The parent post outlines many techniques, including this. Some are more apt than others depending on your language of choice.
I would argue that there's another option of passing a strongly typed struct for args, and that the JavaScript and Ruby solution of dicts falls between this and kwargs.
I'm fond of properly designed fluent APIs, myself.
While on the subject of homogenous argument types, especially bad are the APIs that not only accept the same types, but have order semantics that differ between functions within the same library. PHP used to be a notoriously bad example of this with respect to their needle and haystack search parameters. (I think they fixed it?)
Yeah, I was just showing that in Python keyword arguments are first-class in every respect, which is not that common.
The "JS solution" for example is not a solution, but only a convention: the actual interface is just expecting an object, and there is no explicit check on the validity of such object or its contents. That is nowhere near "strongly typed", if anything it's weaker than average.
Hah I thought that initially, but TypeScript interfaces are simpler (and usually co-sited) compared to their OO cousins. I find them a pleasure to use.
It can, manually (as it can for a non-destructured object), but it has no typing guarantees - the function will still happily be interpreted and run as long as _any_ object is given as an argument.
> I'm fond of properly designed fluent APIs, myself.
Fluent API's are a symptom of a missing language feature, a cascade operator as found in Smalltalk.
anObject
methodOne: a;
methodTwo: b;
methodThree: c.
; does syntactically what returning this does in a fluent API without hijacking the return type of the method to do it and allows you to do it on any method any time without it needed to be designed for use that way.
Inspired by smalltalk, of course. ObjC's verbosity is legendary, and I'm not its hugest fan, but there's a key innovation here that I am a huge fan of: the language forces other people to name their arguments, in much the same way that python forces other people to indent their code, even if they would otherwise default to making a hot mess of things.
My aim isn't to argue that forced named args will cure all that ails you, just to share that they "feel" quite different from optional named args, and that on the balance I felt they were a net positive.
That verbosity isn't even a big deal as you write once, but read many times.
One of the first things about Swift that I fell in love with was the difference between things like DrawRect(10, 20, 30, 40) in languages like C#, and DrawRect(at: origin, width: 30, height: 40) in Swift.
"Reading" code in this sense doesn't just mean explicit review by different people, but also as in reading it subconsciously as you scroll through your own code.
If I see a chain of numbers like 1, 2, 3, 4 my brain has to recall the order of parameters or look it up.
If I'm looking for rectangles that should be wider than they are tall, I have to keep thinking "the third number should be higher than the fourth number"
Oh and I can't just Cmd+F "width:" or "height:" either.
It's unusual to sit down and read a subroutine for pleasure, but it's common to read it when you're debugging a problem, trying to figure out how to extend the subroutine, refactoring something it calls, or doing something similar somewhere else.
Where did that come from? Apart from games that were obviously optimized for Windows first then ported to macOS, has that claim been measured? Because macOS has always certainly FELT faster than Windows.
And then you have to check that the map you're being given has all the correct keys with the correct typed values. And when you're looking at the code, you see:
function DrawRect(options) {...
Unless you're immediately destructuring it or whatever. I don't think it's _that_ straightforward to compare the two, unless I'm thinking of the wrong languages here.
Slightly more verbose but not much. Just having arguments takes you back to square one where you might mix up the order of width and height since nothing is forcing the caller to use named parameters. Perhaps it would be better to have some linter enforcing such things.
> I can understand why this is considered a nice-to-have.
Well typing is one good reason, maps usually are not statically typed with key:value pairs. That's more of a class / struct thing. And then the verbosity increases quite a bit.
You can do that in c#, though an IDE like Rider can also display it like that when no label is provided, or insert the labels, depending on your preference.
IntelliJ does that for Java these days too, and I love it.
I'd rather have syntax-level support (works with ctrl+f, works outside IDEs, works in IDE when auto-analysis is broken), but if IDE support is what I can get, I'll take it.
aThing
outputImageProviderFromBufferWithPixelFormat: a
pixelsWide: b
pixelsHigh: c
baseAddress: d
bytesPerRow: e
releaseCallback: f
releaseContext: g
colorSpace: h
shouldColorMatch: i
This syntax is part of the reason why I really enjoyed working with objective-C, it's just so self-descriptive. Coming from Java which doesn't have that, which relies on an IDE like intellij to give you hints on what arguments go where, OR on design patterns like argument objects or builders, it was a big relief - even if it's kinda long / verbose.
Honestly thinking back on Java there were a lot of design patterns and code (sometimes generated) that could've been avoided with e.g. named function parameters.
Python's kwargs are still optional in the sense that I care about. Same goes for many other kwarg-supporting languages.
The strategy you propose can only force other people to use kwargs when calling functions you write. When you're reading code, there is no guarantee that arguments to functions you didn't write will be labeled, and indeed they often aren't, even when they ought to be. In ObjC, all arguments are labeled, always. It's heavily opinionated, and the experience is qualitatively different as a result.
Did they fix the thing where the first argument can’t be named but the rest can? I kicked the tyres on Swift when it was new and that was a real turn-off.
Yes, however that wasn't the case before SE-0046 (Swift 3): the early iterations had much stronger ties to objective-c where the method "names" the first parameter, so the first parameter could not be labelled separately.
From Swift 3 onwards, parameters and their labelling is completely consistent.
GP is wrong in one place though (at least for swift 2, not sure for swift 1): you could label the first formal parameter explicitly, it just wouldn't be labelled by default (unlike other parameters). That is:
func f(p1:p2:)
would define
f(_:p2:)
but
func f(p1 p1:p2:)
would define
f(p1:p2:)
Also, just to make things weirder, this special-casing would not apply to initialisers.
That was fixed in Swift 3.0 following the acceptation of SE-0046.
In Swift 1.0, this applied to methods but not functions as it was following up from Objectice-C (foo:bar:baz: would become foo(_:bar:baz)), in Swift 2.0 it was expanded to all callables, and in Swift 3.0 it was removed and to be specified explicitly making labelling completely consistent.
If your file operations are designed for it they can recognize the copy situation above and fix it. I don't think it is practical to implement it, but I can design a toy language that would do the above. (Toy because to make it possible to do the above I would have to compromise other things wroth having)
There are lots of usecases where the things you mention are either not important or outright bad. My code also allows for extra data processing steps in between should the use case evolve beyond mere copying.
The code above could, however, just deal with references to some file handle and not actually read or write the contents (though ReadFile() or WriteFile() would be less-than-ideal names for such functionality).
That completely ignores the purpose of the abstraction. Copy is a high level concept that, yes, can sometimes be reduced to a read/write pipeline, but in many cases cannot. Interfaces and abstractions are important concepts that you shouldn't naively discard.
You're also focusing too much on the example, and missing the greater problem. There are other cases that fit the original problem, such a sending an email (who is the sender and who is the receiver?)
My point is that the high-level concept is not always appropriate. Sometimes it's overkill. Sometimes it's outright wrong.
> sending an email (who is the sender and who is the receiver?)
The sender is already encoded in the message itself in that case. The receiver is specified as a specific SMTP message, last I checked. So this problem wouldn't really apply here.
Even if it did, though, the separated functions would still be useful here:
msg = NewMessageFrom("sheev.palpatine@senate.gov")
AddSubject(msg, "Did you ever hear the tragedy of Darth Plagueis the Wise?")
AddBody(msg, "I thought not. It's not a story the Jedi would tell you.")
Send(msg, "anakin.skywalker@jedi.mil")
You can definitely make the API you describe do the things you say it can't, as well as the things other grouchy commenters are saying it can't. But in that case it might be better to rename ReadFile to Source and WriteFile to CopyTo.
this doesn't really solve the problem, its a little better, however, as it STILL accepts positional arguments you can still get in trouble if you don't use parameter names.
A compile time analyzer that says "always use named parameters when calling functions with similar type of consecuitve arguments" should be pretty easy to write.
It would be better to have a foolproof API, but obviously it can't change now.
This is readable and solves the problem the original post is discussing, but it's also nice when learning because an IDE can give very useful help about possible operations, and all you have to remember is the File() function.
(While editing code, position the cursor) on (a class) instance (and press) tab (to swith to) autocomplete (mode and) list all (members of that instance by clicking) on tab (again).
Personally, in C#, I don't enjoy debugging and maintaining Fluent APIs. An exception thrown in the chain is sometimes opaque and the necessary extension methods to make everything come together is not neat code. The former reason is why I also discourage use of object initializers.
I like those API's much better for reading, but when there are issues with them, like you say, debugging is often a right pain compared to the other examples. Especially when a chain became a bit too long (over time) and there is suddenly a null somewhere (for whatever reason). VS indicates the error in the first line of the chain and debugging can become quite hairy.
Is that better in other language runtimes/debuggers?
The thing I like the most about the "as types" approach is that once the thing is wrapped, passing it around has zero boilerplate.
In this program below, the only place that suffers from having created a newtype for the strings is the place where they're created (parseArgs) and where they're used (copyFile). None of the other functions (main, helper1, helper2) ever need to peek inside the type, so there's no boilerplate but still all the type safety.
newtype Source = Source Filename
newtype Dest = Dest Filename
-- Boilerplate at usage
copyFile :: Source -> Dest -> IO ()
copyFile (Source from) (Dest to) = ...
-- Boilerplate at creation
parseArgs :: IO (Source, Dest)
parseArgs = do
[arg1, arg2] <- getArgs
(Source arg1, Dest arg2)
-- Helpers with NO boilerplate
helper1 :: Source -> Dest -> IO ()
helper1 from to = copyFile from to
helper2 :: Source -> Dest -> IO ()
helper2 from to = helper1 from to
main :: IO ()
main = do
(from, to) <- parseArgs
helper2 from to
And by "all the type safety," I mean it's a type error to accidentally swap the args incorrectly to the function call:
> Types are good but having 10 types of string doesn't make sense.
It really does make sense. Types like string, int, float, etc. are poorly defined for most things they represent. There's no context and are just one step away from being dynamic, like values in a dynamic language.
By enforcing a contextual contract (perhaps with additional constructors that constrain the value) you leverage the type-system to do the hard work of checking for stupid mistakes. On large code-bases this reduction in cognitive complexity makes a whole class of bugs go away.
So, 100s of types of string could make sense, it really does depend how many types of thing you have which are distinct and can be represented as a string.
But once they're defined, they're not strings any more, they have an internal state which is a string, yes, but their type is not a string.
The hard part - at least in languages I've worked with - is how hard it is to create a new string that acts like a string should act but isn't a string. In many (C's typedef) you can create two new types, but nothing stops you from using mixing them up as they freely convert to each other without even a warning. In some object oriented languages you can often create a new string - but only by writing by hand every single member function.
Of course none of the above are unsolvable problems. I've never seen anything that actually makes it easy, but I won't claim to have seen everything.
I want to agree, but the problem is that you now need 10 versions for many operations that are in a string library. Hidden assumptions -- like 'replacing a character in a string can never fail' -- may fail (eg in an email address type which only allows only a limited char set). Some constraints can't be expressed without dependent types (say you want to define a URI type that limits the length to 2000 chars total but is composed of variable length components, like the domain name, query strings etc).
> but the problem is that you now need 10 versions for many operations that are in a string library
Do you? Or do you need just the operations for the things that are relevant to the type at hand? Limiting a broad API to the needed interface for the type you're modeling is a GOOD thing.
Not necessarily, you can use coerce or Newtype instances, so you'd import one thing to deal with all the boilerplate wrapper types in all the modules they come from. This is specific to Haskell, but you could mimic it in other languages.
class Source(str):
pass
class Dest(str):
pass
def helper1(a, b):
assert isinstance(a, Source)
assert isinstance(b, Dest)
copyFile(a, b)
if __name__ == “__main__”:
# obvs use argparse instead and store
# directly to Source & Dest objects
# but here to be quick:
a, b = sys.argv[1:3]
a, b = Source(a), Dest(b)
helper1(a, b)
You’re assuming I would endorse this as a good approach in Python in the real world.
I was merely trying to show how to quickly emulate the Haskell example with less code in Python. Both the Haskell approach and asserting on types in Python seem like wastes of time to me personally, and I’d rather just have copyFile by itself and consenting adults can pass the args they want to pass. Use unit tests to make sure you’re not passing paths the wrong way.
Also, what’s with the attitude? “You know how to complicate something very simple” (from ~15 lines of Python?) — geez, you must be a joy to work with.
It addresses exactly the cases you care about, whereas trying to bake it into designs or type system usually adds heavy restrictions that don’t necessarily have anything to do with your exact use cases.
Basically from a You Aren’t Gonna Need It point of view, doing it in tests lets you minimally address the real use case with much less risk of premature or incorrect abstraction.
In my experience, premature abstraction is one of the worst problems in business software. To contrast two far ends of the spectrum, minimalistic patchworks of gross hacking are asymmetrically way better than overhead of premature or incorrect abstractions.
At some point you will have to make a human decision which file to copy, only thing helping you out in that situation is that it clearly states on call-site which arg is src and which arg is dest, instead of remembering left or right argument.
Sure, I mean ultimately you could shoot cosmic rays at your computer to rearrange bits and bypass any type safety you want. I don’t get why you think this bears relevance?
I'm not sure how @Too's example is any worse than named parameters - surely the point is that it's just as likely to transpose source/destination either way.
I think the point is that it’s irrelevant, because type checking or type assertions would take place at the parameter ingestion step specifically to render transposing the arguments impossible.
Any solution could be screwed up on purpose if you’re imagining someone at an interpreter prompt getting the arguments wrong or omitting unit tests that verify a certain file is used as source and a different file as dest... which is why this kind of objection just totally doesn’t matter.
In static typing paradigms, the idea most certainly is to make it foolproof and not merely easy to spot. Literally to convert the problem into something the compiler can prove is unbroken.
Not saying this makes static typing better or anything, just pointing out that “easy to spot in code review” is massively different from “absolute mathematical proof this problem isn’t affecting me.”
I’d argue you rarely care about such compiler proofs in real software development, but that’s beside the point if you assume a static typing paradigm has already been chosen.
But it's the "wrong" solution for python. Python is heavily focused around the idea of duck-typing, and forcing things like this completely blocks duck-typing. What if I want to pass down a complex object that can be used as a string? Python functions should let me pass any string-like thing I want, which is why named arguments are the preferred way to deal with this. You can even force named arguments using kwargs, I believe this is what the boto interface to AWS does.
Your argument doesn’t work. Python affords you tools like metaclasses, __instancecheck__, __subclasshook__, properties and descriptors exactly so you can use the type system to determine how duck typing is handled in whatever cases you want.
Using inheritance and metaprogramming to enforce how and when a given instance or class object satisfies the contract of some interface is a core, fundamental, first-class aspect of Python.
Just because you don’t need to use it very often doesn’t mean “it’s the wrong choice for Python” or anything like that at all.
It is a thing Python goes way out of its way to enthusiastically support, so therefore it is absolutely a Pythonic way to solve problems.
No. Far from it. First of all, what is File in your example? a class? an object? and what does File().Source() return? an object? what does that object represent?
If you want it to be OO, you have a File object, to which you send a "copy" message. Depending on how strictly OO you want it to be, it then returns the new file object (or a reference to it).
I agree that named parameters are a better solution in this case.
A problem I have with the types solution is that, although it's easy to tell if the code is wrong, it's often a bit of a puzzle in my experience to figure out how to write it in the first place: "Hmm, this takes a Foo and a Bar. Can I construct a Foo from a string? No, but I can construct it from a Baz or a Quux. Can I construct a Baz from a string? No, just an int. How about a Quux? Yeah, there we go. Now, how about that Bar?". And I've seen other people have that problem too, especially novice programmers. I'm not a novice but it's still a problem for me in a new type-heavy API.
I realized I was creating that problem in Yeso, and I couldn't figure out how to simplify the API, so in addition to starting the documentation with complete examples, I made a graph with graphviz that shows the type structure of the API: https://gitlab.com/kragen/bubbleos/blob/master/yeso/README.m...
I don't have that problem with named-parameter interfaces (I just look at the list of methods and parameter lists), and maybe I could run into it with fluent interfaces in theory, but in practice that hasn't been much of a problem in my experience.
The article author was writing in Golang, which doesn't have named parameters, but in languages like Python, Lua, Clojure, or, as you say JS, named parameters are straightforward. In Lua it's even simpler than in JS:
copy {from='presentation.md', to='/tmp/backup'}
I would never claim that Lua isn't bug-prone, though!
A language with keyword arguments helps against this. Crystal has a great compromise IMO where you can call the same arguments positionally and as keywords based on the argument name.
The problem with keywords is the compiler can know nothing of intent with them - consider:
CopyFile(from: loaded_file, to: report_file)
Now, somewhere else off in the code someone does something like:
// loaded_file is now the report - so no worries.
loaded_file = report_file
And the compiler then happily propagates that. At no time does it throw an error saying something which would be quite informative such as "Cannot assign report_file (type: outputFile) to loaded_file (type: sourceFile).
Which is really what we want to have happen because - at the very least it forces the developer to go inspect the usage sites to figure out if what they're doing makes sense.
Languages that prefer immutable variables by default would be a better solution for that than introducing two new types, surely. Reassignment of a variable should be a code smell in nearly all contexts that don't involve a loop.
> At no time does it throw an error saying something which would be quite informative such as "Cannot assign report_file (type: outputFile) to loaded_file (type: sourceFile).
Java has InputStream/OutputStream. C++ has istream/ostream. If you use a statically typed language that doesn't have this distinction, blame that specific language's library design. This is orthogonal to keywords.
Is it? Until you open the file as a stream, the data to do so is kicking around your code as a string type with compiler checks on usage. You're making my point: a compile time check is much more helpful.
From your example it looked to me like the types in question were "file" types. You're right that "file name" types are not checked by these languages.
These all help make the code more semantic, more testable, and more securable.
These can also improve more kinds of programming, such as a function that truly needs multiple parameters of the same type (e.g. iterators that are not commutative) or a function that benefits from security aspects (e.g. read-only data).
In some cases this can be fixed by introducing a new type, especially if the language makes it nice to support as a literal. This confers additional benefits in terms of type-checking.
For example, changing an ambiguous foo(float x, float y) into foo(distance x, weight y) .
A nice thing about "everything is an object" languages is that
a.isGreaterThan(b) seems less ambigous than isGreaterThan(a,b). There are natural langauges that are Verb-Subject-Object. It seems less common in natural language to have neither argument be an "agent". Is the copula in the natural language doing the work of the dot?
Adga does mixfix pretty well. A variable in scope can indicate the places for arguments with underscores (e.g. _+_ or _=<_>_). The tradeoff for being able to use pretty much any symbol in identifiers along with mixfix is that you have use whitespace (3 + 5, not 3+5). Though I agree it doesn't scale.
In natural language, "A isGreaterThan B" is a statement and reads as an assertion or a constraint, instead of a conditional test or question; it's ambiguous whether it should do anything. Esperanto has a flexible word order; I think the options would be:
ĉu A estas pli granda ol B? (S-V-O) (lit. "whether A is more big than B?")
ĉu A pli granda ol B estas? (S-O-V)
ĉu estas A pli granda ol B? (V-O-S)
and never "estas pli granda ol A B" as you suggest, but also never avoiding any indication that it's a question.
From a natural language side, it feels very contrived and arbitrary to have one hill measure whether it is bigger than another hill, instead of having an observer measure both 'from the outside'. That suggests the imbalance in syntax between a. and (b) is strange. a > b is more balanced and looks more like an outsider doing the comparison.
> In natural language, "A isGreaterThan B" is a statement and reads as an assertion or a constraint, instead of a conditional test or question; it's ambiguous whether it should do anything.
The reason functions with these names (and comparator operators) return booleans is precisely to make the code read like natural language. Look at the C syntax:
if ( a > b ) {
doSomething();
}
We read this is "if a is greater than b, do something", and by an amazing coincidence, that's what the code does. In order to get the code to do what it says it will do, we need the > operator to be a conditional test. It always looks like a conditional test because it always appears in the explicit context of a conditional test. The fact that "a > b" outside of a conditional context looks like a constraint rather than a test isn't really relevant, because placing it outside of that context in the code is a no-op and therefore doesn't come up.
I think infix operators are even better for binary functions like this: a <= b. Haskell uses this to great effect (though some people think operators look like noise).
Languages and APIs designed like this and get you crippled tools that simply do not allow you to express about half the shit you're gonna need to do. That generates another layer of indirection, abstractions, and workarounds to break through the "we think for you" for when what "they thought for you" wasn't what you was actually thunking.
There's a place for languages that don't allow you to shoot yourself in the foot or other member of your choice, I'm sure. I don't want to go there.
The idea is to use the type system to help you check your work. If programmers were anywhere near diligent about minding the docs, static typing would be far less useful than it is. Always assume the programmer hasn't had their coffee and is prone to making mistakes, and design your system to catch them as early as possible.
> Languages and APIs designed like this and get you crippled tools that simply do not allow you to express about half the shit you're gonna need to do.
What problems would you see arising (either directly or indirectly) from the specific suggestion in the article, for example?
If an API requires that I re-check the docs to remind me of something simple like whether or not the source or destination comes first, the API is bad.
This isn't about removing flexibility, it's about helping people to avoid making easy mistakes that could be prevented.
I remember the order for ln -s because the third argument is optional. If you omit the third argument the command will create a symbolic link in the current directory with the same filename as the original.
IntelliJ labels the parameter at the call site with its parameter name, if its value isn't a local variable that already has that name. On your display it looks like CopyFile(from: "foo", to: "bar"), with the from and to labels in an unobtrusive font, but in the source file it's just CopyFile("foo", "bar").
You don't want it unlabeled in the source code though - a code reviewer will totally miss a swapping of parameters. It's best when fully supported and explicit in the language.
It's due to how language and entropy work; basically people naturally tend to abbreviate frequently used terms.
Some languages have banned positional arguments, like Obj C, but these do get a reputation for being verbose. And verbosity, past a point, hinders readability.
My inclination is that for unary and binary functions, positional arguments should be sugar for the keyword arguments, and anything beyond that must be keywords.
An alternative rule could be you're allowed to designate one positional argument.
Keep in mind, also, that I'll bet there's code in keyword-only languages that just uses single letters for all the keywords. Some people just won't or can't write readable code.
In C++ one could use strong types (template phantom types with anonymous tags) to distinguish between source and destination.
I use this technic to disambiguate all kind of types (source/destination gpu/cpu pointers for instance) and correctly dispatch everything at compile time.
This is really the number one reason I'd like to use F# more. Single case unions and units of measure are really slick, and while you can do the same thing in C#, the legwork is pretty high.
class Hours : NewType<Hours, double> {
public Hours(double x) : base(x) {}
}
It gives you:
* Equality operators, Equals, and IEquatable
* Ordering operators, CompareTo, and IComparable
* GetHashCode, ToString
* Map, Select, Bind, SelectMany, Fold, ForAll, Exists, Iter
* Explict conversion operator to convert from the NewType to the internal wrapped type
* Serialisation
* Hours.New(x) constructor function (often useful when used with other methods that take first-class functions).
It's even possible with the more complex variants to provide predicates that will run on construction to constrain the values going in.
There's also NumType for integer numeric types like int, long, etc. and FloatType for floating-point numeric types like float, double, etc. They give additional functionality by default.
Also, units of measure [4]:
Length x = 10*inches;
Area y = x * x;
Time t = 10*sec;
Erm, no it doesn't. It has the ability to define units of measure, but the language doesn't have UoM built-in. There are UoM in FSharp.Core [1], so it's the same as including it from a library.
Obviously F#’s first class support for UoM is better than my implementation, but for all intents and purposes the behaviour is the same.
Most people would consider 'distributed with F#' as being equivalent to 'built in to F#'. Actually it's even cooler that the SI units are in the standard library rather than baked into the language.
There was a post here not long ago, by jerf iirc, called something like 'tiny types in go.' At the time I disagreed with the premise, but I think it would be a cleaner solution here. By aliasing two string types with obvious names, it forces you to convert at calltime. Assuming you name source and dest as types(ie type source string), you end up with a command like copy(source(sourcefile), dest(destfile)). It's cleaner and has less abstraction than what's in the article.
There are also techniques like type tags and refinement types in some programming languages, which serve a similar effect. It's really useful for marking data as validated or as earmarked for a given purpose as it flows through layers of a system.
I used to feel guilty for how much I use Google, developer.mozilla.org and Stack Overflow, but after a series of languages and a herd of APIs, it's a blur to remember which order is correct for which library, and how they deal with inputs that are empty or undefined.
It's safer to look it up, peruse the complaints about how it does weird shit with negative numbers or empty string, and then write my code standing on those shoulders.
I spend enough time stepping through other people's code even when I do this stuff. It's really not that interesting and certainly not fun.
Amen to that. And it goes double when you're hopping around different parts of a multidisciplinary project and probably only using a given language or framework once or twice a year for a couple of weeks. These days I find myself looking things up just on principle, even when I'm reeeasonably sure I remember how they work. It just saves time in the long run.
I had a project with three implementations of merge (including lodash) and they all worked differently. After I fixed a handful of other people’s bugs due to using one and expecting the behavior of another, I felt a lot less guilty.
To be honest, I prefer the simple form. It's immediately obvious what it does, and the cognitive load when working with it is slightly lower. That justifies the extra second or so you need to view the function signature and make sure your arguments are in the right order. "Clever" solutions to anticipated problems that haven't happened yet frequently end up being tech debt down the line.
241 comments
[ 3.4 ms ] story [ 363 ms ] threadOften one parameter is distinguished by whether it could be an array. The Unix cat utility takes multiple sources to a single destination, so analogously:
and it'll be obvious in the call site what's going on: and your function can also concatenate files.(Those that don't will need to pass in named tuples, or structs, or similar data structures.)
I had an adventure upgrading versions of the .NET Core Rabbit library: one of the (boolean) parameters of the Consume function used to be “noAck”, but it got changed to “autoAck” (with inverted behavior). Even though the change was well documented, I would have preferred that to be a breaking change at compile time...
The parent post outlines many techniques, including this. Some are more apt than others depending on your language of choice.
I would argue that there's another option of passing a strongly typed struct for args, and that the JavaScript and Ruby solution of dicts falls between this and kwargs.
I'm fond of properly designed fluent APIs, myself.
While on the subject of homogenous argument types, especially bad are the APIs that not only accept the same types, but have order semantics that differ between functions within the same library. PHP used to be a notoriously bad example of this with respect to their needle and haystack search parameters. (I think they fixed it?)
The "JS solution" for example is not a solution, but only a convention: the actual interface is just expecting an object, and there is no explicit check on the validity of such object or its contents. That is nowhere near "strongly typed", if anything it's weaker than average.
Well if it uses de-structuring syntax, then it can be type-checked.
Fluent API's are a symptom of a missing language feature, a cascade operator as found in Smalltalk.
; does syntactically what returning this does in a fluent API without hijacking the return type of the method to do it and allows you to do it on any method any time without it needed to be designed for use that way.No, they put out a conf talk that stated that there _was_ a logic to it, you just needed it to be explained to you.
But nobody talks about how nonsensical C function names are.
one can still make a hot mess of 1000-wide lines, bad variable names, and various other forms of spaghetti cowboy-ism.
My aim isn't to argue that forced named args will cure all that ails you, just to share that they "feel" quite different from optional named args, and that on the balance I felt they were a net positive.
One of the first things about Swift that I fell in love with was the difference between things like DrawRect(10, 20, 30, 40) in languages like C#, and DrawRect(at: origin, width: 30, height: 40) in Swift.
One of the most pernicious myths of software engineering is that it happens.
If I see a chain of numbers like 1, 2, 3, 4 my brain has to recall the order of parameters or look it up.
If I'm looking for rectangles that should be wider than they are tall, I have to keep thinking "the third number should be higher than the fourth number"
Oh and I can't just Cmd+F "width:" or "height:" either.
https://docs.microsoft.com/en-us/dotnet/api/system.drawing.r...
function DrawRect(options) {...
Unless you're immediately destructuring it or whatever. I don't think it's _that_ straightforward to compare the two, unless I'm thinking of the wrong languages here.
It will be checked compile time that all keys are provided (unless dict comes from serialized json or something).
Well typing is one good reason, maps usually are not statically typed with key:value pairs. That's more of a class / struct thing. And then the verbosity increases quite a bit.
I'd rather have syntax-level support (works with ctrl+f, works outside IDEs, works in IDE when auto-analysis is broken), but if IDE support is what I can get, I'll take it.
Honestly thinking back on Java there were a lot of design patterns and code (sometimes generated) that could've been avoided with e.g. named function parameters.
And if you want, you can make arguments keyword-only:
Parameters after the asterisk cannot be specified by positional arguments. It must be called with keyword arguments:The strategy you propose can only force other people to use kwargs when calling functions you write. When you're reading code, there is no guarantee that arguments to functions you didn't write will be labeled, and indeed they often aren't, even when they ought to be. In ObjC, all arguments are labeled, always. It's heavily opinionated, and the experience is qualitatively different as a result.
From Swift 3 onwards, parameters and their labelling is completely consistent.
GP is wrong in one place though (at least for swift 2, not sure for swift 1): you could label the first formal parameter explicitly, it just wouldn't be labelled by default (unlike other parameters). That is:
would define but would define Also, just to make things weirder, this special-casing would not apply to initialisers.In Swift 1.0, this applied to methods but not functions as it was following up from Objectice-C (foo:bar:baz: would become foo(_:bar:baz)), in Swift 2.0 it was expanded to all callables, and in Swift 3.0 it was removed and to be specified explicitly making labelling completely consistent.
This is Wrong, with a capital W.
The file copy API is fundamentally different to reading and and then writing the contents of a file.
The "copy" system call on most platforms also:
- Copies metadata such as attributes.
- Copies alternate data streams.
- Preserves advanced features such as sparse files, compression, encryption, BTRFS/ReFS/ZFS integrity settings.
- Can copy externally archived files without restoring them.
- Can copy files server-to-server without transferring the data to the client computer.
- Copies block-by-block instead of all at once, allowing files to be copied that do not fit into memory.
- Copies asynchronously so that reads and writes are overlapped
- Etc...
The code above could, however, just deal with references to some file handle and not actually read or write the contents (though ReadFile() or WriteFile() would be less-than-ideal names for such functionality).
You're also focusing too much on the example, and missing the greater problem. There are other cases that fit the original problem, such a sending an email (who is the sender and who is the receiver?)
My point is that the high-level concept is not always appropriate. Sometimes it's overkill. Sometimes it's outright wrong.
> sending an email (who is the sender and who is the receiver?)
The sender is already encoded in the message itself in that case. The receiver is specified as a specific SMTP message, last I checked. So this problem wouldn't really apply here.
Even if it did, though, the separated functions would still be useful here:
CopyFile(src: "foo", dest: "bar");
It would be better to have a foolproof API, but obviously it can't change now.
e.g.
neither would seem to show which is correct without knowing what the path values are if the variable names are ambiguous.If you can't keep track of your variable names, then using keyword arguments, like others here talk about, won't help much, either.
I'd definitely agree with you that the not having named arguments is worse than your method chained example.
To me named parameters seem to be on a par with the chained OO api.
Funny, I don't think I've actually seen a fluent file API like this before. I guess most languages have file I/O built in to their standard libraries.
My immediate reaction to this, though, is that you could make it more general:
You could then also have This is readable and solves the problem the original post is discussing, but it's also nice when learning because an IDE can give very useful help about possible operations, and all you have to remember is the File() function.Why is chaining necessary for this? On-instance tab-autocomplete list-all on-tab accomplishes the same thing.
I have no idea what you just said.
(While editing code, position the cursor) on (a class) instance (and press) tab (to swith to) autocomplete (mode and) list all (members of that instance by clicking) on tab (again).
(e.g. foo.[TAB])
can list all possible operations available on that instance -- in other words, you don't need a fluent interface for this.
Is that better in other language runtimes/debuggers?
```c# var file = File("source"); file.CopyTo("target"); ```
(apologies if this is formatted badly; no clue what HN supports here, and I'm on mobile)
In this program below, the only place that suffers from having created a newtype for the strings is the place where they're created (parseArgs) and where they're used (copyFile). None of the other functions (main, helper1, helper2) ever need to peek inside the type, so there's no boilerplate but still all the type safety.
And by "all the type safety," I mean it's a type error to accidentally swap the args incorrectly to the function call:You could "force" parameter naming, and it would be easier.
Types are good but having 10 types of string doesn't make sense.
Not if the strings are clearly distinct (like a name being different from an e-mail address).
It really does make sense. Types like string, int, float, etc. are poorly defined for most things they represent. There's no context and are just one step away from being dynamic, like values in a dynamic language.
By enforcing a contextual contract (perhaps with additional constructors that constrain the value) you leverage the type-system to do the hard work of checking for stupid mistakes. On large code-bases this reduction in cognitive complexity makes a whole class of bugs go away.
So, 100s of types of string could make sense, it really does depend how many types of thing you have which are distinct and can be represented as a string.
But once they're defined, they're not strings any more, they have an internal state which is a string, yes, but their type is not a string.
Of course none of the above are unsolvable problems. I've never seen anything that actually makes it easy, but I won't claim to have seen everything.
It's not. It's 10 types.
Do you? Or do you need just the operations for the things that are relevant to the type at hand? Limiting a broad API to the needed interface for the type you're modeling is a GOOD thing.
Add exhaustive pattern matches and I was hooked
What is the advantage here? As I would prefer to have the authors original problem than this.
I was merely trying to show how to quickly emulate the Haskell example with less code in Python. Both the Haskell approach and asserting on types in Python seem like wastes of time to me personally, and I’d rather just have copyFile by itself and consenting adults can pass the args they want to pass. Use unit tests to make sure you’re not passing paths the wrong way.
Also, what’s with the attitude? “You know how to complicate something very simple” (from ~15 lines of Python?) — geez, you must be a joy to work with.
Basically from a You Aren’t Gonna Need It point of view, doing it in tests lets you minimally address the real use case with much less risk of premature or incorrect abstraction.
In my experience, premature abstraction is one of the worst problems in business software. To contrast two far ends of the spectrum, minimalistic patchworks of gross hacking are asymmetrically way better than overhead of premature or incorrect abstractions.
Any solution could be screwed up on purpose if you’re imagining someone at an interpreter prompt getting the arguments wrong or omitting unit tests that verify a certain file is used as source and a different file as dest... which is why this kind of objection just totally doesn’t matter.
Not saying this makes static typing better or anything, just pointing out that “easy to spot in code review” is massively different from “absolute mathematical proof this problem isn’t affecting me.”
I’d argue you rarely care about such compiler proofs in real software development, but that’s beside the point if you assume a static typing paradigm has already been chosen.
Using inheritance and metaprogramming to enforce how and when a given instance or class object satisfies the contract of some interface is a core, fundamental, first-class aspect of Python.
Just because you don’t need to use it very often doesn’t mean “it’s the wrong choice for Python” or anything like that at all.
It is a thing Python goes way out of its way to enthusiastically support, so therefore it is absolutely a Pythonic way to solve problems.
No. Far from it. First of all, what is File in your example? a class? an object? and what does File().Source() return? an object? what does that object represent?
If you want it to be OO, you have a File object, to which you send a "copy" message. Depending on how strictly OO you want it to be, it then returns the new file object (or a reference to it).
Stop! Put down the editor and step away from the compiler, please. No one wants to get hurt.
A problem I have with the types solution is that, although it's easy to tell if the code is wrong, it's often a bit of a puzzle in my experience to figure out how to write it in the first place: "Hmm, this takes a Foo and a Bar. Can I construct a Foo from a string? No, but I can construct it from a Baz or a Quux. Can I construct a Baz from a string? No, just an int. How about a Quux? Yeah, there we go. Now, how about that Bar?". And I've seen other people have that problem too, especially novice programmers. I'm not a novice but it's still a problem for me in a new type-heavy API.
I realized I was creating that problem in Yeso, and I couldn't figure out how to simplify the API, so in addition to starting the documentation with complete examples, I made a graph with graphviz that shows the type structure of the API: https://gitlab.com/kragen/bubbleos/blob/master/yeso/README.m...
I don't have that problem with named-parameter interfaces (I just look at the list of methods and parameter lists), and maybe I could run into it with fluent interfaces in theory, but in practice that hasn't been much of a problem in my experience.
The article author was writing in Golang, which doesn't have named parameters, but in languages like Python, Lua, Clojure, or, as you say JS, named parameters are straightforward. In Lua it's even simpler than in JS:
I would never claim that Lua isn't bug-prone, though!```c# var source = new FileSystemFile("source"); source.CopyTo("target"); ```
Of course, this isn't the only way to do it; you get the usual footguns as described in the article too.
Also apologies if the formatting is crappy - on mobile, and TBH not sure what markdown-like syntax HN supports, despite being a long-time user!
edit: forgot that flake8 doesn't check for mutable arguments by default – need to add https://github.com/PyCQA/flake8-bugbear
Which is really what we want to have happen because - at the very least it forces the developer to go inspect the usage sites to figure out if what they're doing makes sense.
Java has InputStream/OutputStream. C++ has istream/ostream. If you use a statically typed language that doesn't have this distinction, blame that specific language's library design. This is orthogonal to keywords.
These can also improve more kinds of programming, such as a function that truly needs multiple parameters of the same type (e.g. iterators that are not commutative) or a function that benefits from security aspects (e.g. read-only data).
For example, changing an ambiguous foo(float x, float y) into foo(distance x, weight y) .
Mary.give(John, apple)
ĉu A estas pli granda ol B? (S-V-O) (lit. "whether A is more big than B?")
ĉu A pli granda ol B estas? (S-O-V)
ĉu estas A pli granda ol B? (V-O-S)
and never "estas pli granda ol A B" as you suggest, but also never avoiding any indication that it's a question.
From a natural language side, it feels very contrived and arbitrary to have one hill measure whether it is bigger than another hill, instead of having an observer measure both 'from the outside'. That suggests the imbalance in syntax between a. and (b) is strange. a > b is more balanced and looks more like an outsider doing the comparison.
The reason functions with these names (and comparator operators) return booleans is precisely to make the code read like natural language. Look at the C syntax:
We read this is "if a is greater than b, do something", and by an amazing coincidence, that's what the code does. In order to get the code to do what it says it will do, we need the > operator to be a conditional test. It always looks like a conditional test because it always appears in the explicit context of a conditional test. The fact that "a > b" outside of a conditional context looks like a constraint rather than a test isn't really relevant, because placing it outside of that context in the code is a no-op and therefore doesn't come up.Yeah. Mind your docs.
Languages and APIs designed like this and get you crippled tools that simply do not allow you to express about half the shit you're gonna need to do. That generates another layer of indirection, abstractions, and workarounds to break through the "we think for you" for when what "they thought for you" wasn't what you was actually thunking.
There's a place for languages that don't allow you to shoot yourself in the foot or other member of your choice, I'm sure. I don't want to go there.
What problems would you see arising (either directly or indirectly) from the specific suggestion in the article, for example?
This isn't about removing flexibility, it's about helping people to avoid making easy mistakes that could be prevented.
Of course, the solution is to remember that ln is in the same family as cp and mv; ls is a different family.
Some languages have banned positional arguments, like Obj C, but these do get a reputation for being verbose. And verbosity, past a point, hinders readability.
My inclination is that for unary and binary functions, positional arguments should be sugar for the keyword arguments, and anything beyond that must be keywords.
An alternative rule could be you're allowed to designate one positional argument.
Keep in mind, also, that I'll bet there's code in keyword-only languages that just uses single letters for all the keywords. Some people just won't or can't write readable code.
I use this technic to disambiguate all kind of types (source/destination gpu/cpu pointers for instance) and correctly dispatch everything at compile time.
The compiler becomes your best friend then. :)
However, after your comment, I decided to spend a few minutes on the topic, and came up with these links:
https://www.fluentcpp.com/2016/12/08/strong-types-for-strong...
https://www.fluentcpp.com/2018/12/14/named-arguments-cpp/
I am now looking forward to refactor some of our code base to make use of this. Awesome and thanks for the nudge!
* Equality operators, Equals, and IEquatable
* Ordering operators, CompareTo, and IComparable
* GetHashCode, ToString
* Map, Select, Bind, SelectMany, Fold, ForAll, Exists, Iter
* Explict conversion operator to convert from the NewType to the internal wrapped type
* Serialisation
* Hours.New(x) constructor function (often useful when used with other methods that take first-class functions).
It's even possible with the more complex variants to provide predicates that will run on construction to constrain the values going in.
There's also NumType for integer numeric types like int, long, etc. and FloatType for floating-point numeric types like float, double, etc. They give additional functionality by default.
Also, units of measure [4]:
[1] NewType - https://github.com/louthy/language-ext/tree/master/LanguageE...[2] NumType - https://github.com/louthy/language-ext/tree/master/LanguageE...
[3] FloatType - https://github.com/louthy/language-ext/tree/master/LanguageE...
[4] Units of Measure - https://github.com/louthy/language-ext/tree/master/LanguageE...
Obviously F#’s first class support for UoM is better than my implementation, but for all intents and purposes the behaviour is the same.
[1] https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSha...
I have seen your lib before - nice work.
Developing C# in rider if you call `foo(a,b)` then it'll display as `foo(from: a, to: b)` in the IDE.
I write my libraries like this because i want to beat my users into being explicit -- thus the "a little autocratic, but.." caveat.
It's safer to look it up, peruse the complaints about how it does weird shit with negative numbers or empty string, and then write my code standing on those shoulders.
I spend enough time stepping through other people's code even when I do this stuff. It's really not that interesting and certainly not fun.
It detects if your function signature is
> foo(first, second)
and you call it with
> foo(second, first)
that is, it actually checks for variable names that are mismatched, with comments like
> foo(/* first= * / second, /* second= * / first)
as an escape hatch.