241 comments

[ 3.4 ms ] story [ 363 ms ] thread
I'm not a fan of the CopyFile solution suggested.

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:

  bool cat_files_to_file(string dst, vector<string> srcs)
and it'll be obvious in the call site what's going on:

  cat_files_to_file("/tmp/backup", {"presentation.md"});
and your function can also concatenate files.
Many languages have some concept of "keyword arguments".

(Those that don't will need to pass in named tuples, or structs, or similar data structures.)

Here's a variation of the example which I like a bit better:

  type CopyFile string
  
  func (src CopyFile) To(dest string) error {
       // copy file here
  }

  func main() {
       CopyFile("presentation.md").To("/tmp/backup")
  }
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...

KW's:

  CopyFile(from: 'foo', to: 'bar')
or JS:

  CopyFile({from: 'foo', to: 'bar'})
or as types:

  CopyFile(Source('foo'), Dest('bar'))
or as fluent:

  Copy.from('foo').to('bar')
or as Python:

   shutil.copy(src='foo', dst='bar')
That's straight out of stdlib, btw.
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.

> The "JS solution" for example is not a solution, but only a convention: the actual interface is just expecting an object,

Well if it uses de-structuring syntax, then it can be type-checked.

De-structuring syntax isn't required, this is just as easily type-checked:

    interface CopyArgs {
        src: string;
        dst: string;
    }

    function copy(args: CopyArgs) {
        exec(["cp", args.src, args.dst])
    }
Sure, if you want to get all interfacy - but pretty soon it'll be Java :)
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.
> (I think they fixed it?)

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.

Really, most of the relevant PHP functions are just thin wrappers around the C functions with the same name.

But nobody talks about how nonsensical C function names are.

I love this part of Python. I can choose to include the argument names if I think they tell a more clear story.
or as Objective C:

    [[NSFileManager defaultManager] copyItemAtPath:src toPath:target error:&error];
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.
> otherwise default to making a hot mess of things.

one can still make a hot mess of 1000-wide lines, bad variable names, and various other forms of spaghetti cowboy-ism.

Yep, no doubt about it.

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.

> That verbosity isn't even a big deal as you write once, but read many times.

One of the most pernicious myths of software engineering is that it happens.

"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.

I would enjoy an explanation of your thinking.
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.
Named function parameters are nice, but you can accomplish something similar with only two more characters in some languages:

      DrawRect({at: origin, width: 30, height: 40})
  vs. DrawRect(at: origin, width: 30, height: 40)
I can understand why this is considered a nice-to-have. The verbosity didn't really change.
Two more characters and one more allocation per each LoC.
Most of us are just writing crud apps anyway so one more allocation is extremely affordable
Maybe but this highly dynamic programming style is why Macs have always been so much slower than Windows on equivalent hardware.
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.
As if my app stops working because of this?
You can accomplish it in other languages, but will all the other library authors do so? Do the OS libraries do so?
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.

Typescript can specify required keys in a dictionary: function DrawRect(options: { at: Point2D, width: number, height:number}}

It will be checked compile time that all keys are provided (unless dict comes from serialized json or something).

That's more verbose than just having arguments though, right?
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.
It doesn't take you back to square one in languages which enforce named parameters, which I thought we were talking about.
> 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.

  [NSCarpalTunnelSyndromeInducer induceCarpalTunnelSyndromeForProgrammer: ....]
Or, ya know, you use XCode and it autocompletes the function signature for you.
XCode is really unpleasant as far as IDEs go.
Okay then

     [NSEyeStrainInducer induceEyeStrainForProgrammer: ....]
Unless XCode can read the signature for me in addition to writing it
Good news, it can! Either way objective-c is much more readable than it is writeable. It’s writeable with an IDE and readable without.
[NSStream dammit]
outputImageProviderFromBufferWithPixelFormat:pixelsWide:pixelsHigh:baseAddress:bytesPerRow:releaseCallback:releaseContext:colorSpace:shouldColorMatch

    aThing
       outputImageProviderFromBufferWithPixelFormat: a
       pixelsWide: b
       pixelsHigh: c
       baseAddress: d
       bytesPerRow: e
       releaseCallback: f
       releaseContext: g
       colorSpace: h
       shouldColorMatch: i
Reads well.
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.

In Python, by default you can use keyword arguments for any function parameter.

And if you want, you can make arguments keyword-only:

  def copyFile(*, source, dest):
      ...
Parameters after the asterisk cannot be specified by positional arguments. It must be called with keyword arguments:

  copyFile(source=src, dest=dst)
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.

Swift has keyword parameter for function parameters, as in copy(from: "foo", to: "bar")
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.
The first argument can be named just fine.

    func fn1(myLabel1 myParam1: int, myLabel2 myParam2: int) {
    }

    fn1(myLabel1: 123, myLabel2: 456)

    func fn2(_ myParam1Omitted: int, myLabel2 myParam2: int) {
    }

    fn2(123, myParam2: 456)
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.

Or sidestep this problem entirely by being explicit:

    contents = ReadFile('foo')
    WriteFile('bar', contents)
Or more concisely:

    WriteFile('bar', ReadFile('foo'))
(Of course, this would likely preclude the ability to do some means of copying that doesn't involve reading the whole file into memory).
Please don't ever recommend such code, even in jest.

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...

- can be almost instant and copy only metadata on CoW filesystems
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?)

> Copy is a high level concept

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.
or C#:

CopyFile(src: "foo", dest: "bar");

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.

c# already has the 'chained' API in the standard library:

    new FileInfo(srcPath).CopyTo(dstPath)
On the other hand, it also has the more error-prone

    File.Copy(sourceFileName,destFileName);
I know nothing about C# so please pardon my ignorance but how does:

  new FileInfo(srcPath).CopyTo(dstPath);
provide more protection vs

  CopyFile(src: srcPath, dest: dstPath);
Assuming that both path strings are set correctly isn't a typo here going to cause the same error and be just as visible?

e.g.

  new FileInfo(pathA).CopyTo(pathB);
  CopyFile(src: pathB, dest: pathA);
neither would seem to show which is correct without knowing what the path values are if the variable names are ambiguous.
When using the FileInfo api, it's obvious which argument is the source and which argument is the destination. When using the CopyFile, it's not.

If you can't keep track of your variable names, then using keyword arguments, like others here talk about, won't help much, either.

Sorry, not sure if your parent was edited to remove keynamed parameters from the second example or if I've responded to the wrong comment.

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.

Ah, the reason is simple: C# allows named parameters, but do not enforce them. You can call File.Copy both as

    File.Copy(a,b)
and

    File.Copy(sourceFileName: a, destFileName: b)
And confusingly, also as

    File.Copy(destFileName: b, sourceFileName: a)
OK I see. That explains it and also gives a very concrete reason for using the chained API. Thanks.
> Copy.from('foo').to('bar')

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:

    File('foo').CopyTo('bar');
You could then also have

    .ReadAsText()

    .MoveTo(target)

    .SetOwner(owner)

    .GetFullPath() 
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.
> also nice when learning because an IDE can give very useful help about possible operations

Why is chaining necessary for this? On-instance tab-autocomplete list-all on-tab accomplishes the same thing.

> On-instance tab-autocomplete list-all on-tab

I have no idea what you just said.

I think it means:

(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).

He said that clicking TAB on an instance in an IDE

(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.

Your suggestion is basically Ruby’s `Pathname` class (sans convenient copy method).
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 proper place to pay the dev cost is once in the library, not in all its callers.
If you want to be more opaque, I guess you could do:

```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)

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:

    (from, to) <- parseArgs
    helper2 to from
Yes but creating the scaffolding is the hard part and getting to the "once the thing is wrapped" takes more time than the other solutions.

You could "force" parameter naming, and it would be easier.

Types are good but having 10 types of string doesn't make sense.

> 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).

> 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.

You are conflating dynamic types with weak types. Int is a relatively weak type and HttpPort is a strong type regardless whether static or dynamic
> 10 types of string

It's not. It's 10 types.

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.

This is the killer feature that made me sit up and take notice of F#.

Add exhaustive pattern matches and I was hooked

In another module, there's the boilerplate of importing Source and Dest.
They get brought in alongside the functions. Especially given Haskell (sadly) defaults to unqualified import.
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.
In Python I might do something like this.

    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 know how to complicate something very simple.

What is the advantage here? As I would prefer to have the authors original problem than this.

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.

How is writing extra boilerplate tests better than saying what you mean in the primary code?
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.

Python has named arguments; you can use something like:

    copy(source=file1, destination=file2)

and be done with it. Or am I missing something?
This would do nothing to prevent passing it like

    copy(source=file2, dest=file1)
whereas the parent post is trying to use the type system to make this sort of mistake much more difficult to make.
Parent would not stop me from doing this either:

    copy(source=Src(file2), dest=Dest(file1))
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.

Such an error would be immediately obvious on code review. The idea is to make it obvious, not foolproof.
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.

Levels of indirection are not a “code smell” but they offer many more opportunities for bugs than parameter list order does.
new File().Source("xyz").CopyTo("zyx") is the cleanest I believe, and also the most (real) OO way.
> and also the most (real) OO way.

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).

In the fluent scenario, I will be waiting for the instruction to execute:

   Copy.from('foo').to('bar').execute();
Because of variations like these:

   Copy.from('foo').to('bar').owner('me');
   Copy.to('bar').from('foo');
As C:

  struct copy_args_s {
    char * src;
    size_t src#;
    char * dest;
    size_t dest#;
  };
  int copy(struct copy_args_s args) {
    ...
  }
  int main() {
    copy((struct copy_args_s){ .src = "foo", .src_len = 3, .dst = "bar", .dst_len = 3 });
  }

Or with some macro magic:

  struct copy_args_s {
    char * src;
    size_t src#;
    char * dest;
    size_t dest#;
  };
  int copy(struct copy_args_s args) {
    ...
  }

  #define copy(...) copy((struct copy_args_s){__VA_ARGS__})

  int main() {
    copy(.src = "foo", .src_len = 3, .dst = "bar", .dst_len = 3);
  }
> Or with some macro magic

Stop! Put down the editor and step away from the compiler, please. No one wants to get hurt.

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!
for things that have clear direction,

    'foo' `copyTo` 'bar'
You can get something like your last example in c#:

```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!

Destructured object parameters make this easy in TypeScript (and JS):

  async function copyFile({
    source,
    dest,
  }: {source:string, dest:string}): Promise<void> {
    // ...
  }

  await copyFile({source: 'presentation.md', dest: '/tmp/backup'});
There's no need to destructure the parameters.
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.
same goes with python -

  def foo(x=None, y=None):
    ...

  foo(x=1, y=2)
  foo(1, y=2)
  foo(1, 2)
You can also disable positional handling via * to require keywords in function calls, e.g.:

    def add(*, l, r):
      return l + r
    
    add(l=1, r=2)
    >>> 3
    add(1, r=2)
    >>> [...]
    >>> TypeError: add() takes 0 positional arguments but 1 positional argument (and 1 keyword-only argument) were given
I believe 3.8 is adding / (I think) to do the same but for positional arguments, i.e., arguments you cannot give by keyword.
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 least that can be determined to be buggy without checking the documentation of CopyFile
> 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.
IMHO this concept is better solved by using semantics, such as types, keyword parameters, interfaces, traits, etc:

    copy(source: ReadOnlyFile, destination: WriteOnlyFile)
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) .

For a second, based on the domain, I wondered if Dick Cheney had re-invented himself as a developer.
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?
That doesn't scale past two arguments.
You can use fluent interfaces to get past two args:

    myObject.doThing(a).toAthing(b)
Though that requires a good amount of boilerplate on the part of the library author.
Mary.give(apple).to(John)

Mary.give(John, apple)

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 Haskell you can make any (prefix) function infix (and vice versa). Some other languages (eg. Kotlin) support this as well.

  a + b     -- '(+)' function used infix
  (+) a b   -- '(+)' function used prefix

  min a b   -- 'min' function used prefix
  a `min` b -- 'min' function used infix
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).
> Can you suggest a better solution?

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.

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.

The solution provided by OP is what I try to avoid at all costs. It’s the beginning of spaghetti code.
I agree, and I am not still sure if he is serious.
This. It extends to command line tools too. I can _never_ remember what the order is for `ln -s`.
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.
I remember this usage the exact same way :)
Unix tools are pretty consistent that source comes before destination. If you get stuck, just remember that it's the same as cp, mv, etc.
I think the confusion comes because you have to do

    ln -s bar foo
so that "ls -l" then prints

    foo -> bar
People who look at an existing symlink before trying to create a new one will get confused.

Of course, the solution is to remember that ln is in the same family as cp and mv; ls is a different family.

that’s one of the reason why I like languages that allow you to write this:

    thing(dest=a, src=b
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").
Not just IntelliJ, all their IDE’s
When someone says "IntelliJ" they are often referring to any and all of the JetBrains IDEs. They are all slightly different versions of IntelliJ.
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.
Yup! It's nice and all that IntelliJ ghosts the parameter names in there, but that does nothing for external review, static analysis, etc.
Positional arguments just shouldn't exist. Why they persist in newer languages like Go is a mystery.
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.

Unary, yes. Binary, no.
(comment deleted)
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.

The compiler becomes your best friend then. :)

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.
In my language-ext library it's pretty trivial:

    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;
[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...

Of course F# has units-of-measure built-in
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.

[1] https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSha...

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.
It's pretty clear what I meant - that F# has support of UoM's. Where the canned ones live is is neither here nor there.

I have seen your lib before - nice work.

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.
a way to avoid this in python that i quite like is to require keyword arguments:

  def foo(*, x, y):
      pass
  
  # you must do this
  foo(x=5, y=6)

  # you cannot do this
  foo(5,6)

  # and therefore you also may not
  foo(6,5)
This is a little autocratic but the other nice thing is that if you’re finding it really painful to use it means you should probably refactor anyway.
I prefer to let the IDE do this.

Developing C# in rider if you call `foo(a,b)` then it'll display as `foo(from: a, to: b)` in the IDE.

All Jetbrains IDE do this (AFAIK). One can even configure the cases when these are displayed (ie. only show the hints when there is an ambiguity)
for my own uses, that's great.

I write my libraries like this because i want to beat my users into being explicit -- thus the "a little autocratic, but.." caveat.

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.
ErrorProne has a nice safeguard against this if you're writing Java code: https://github.com/google/error-prone/blob/master/docs/bugpa...

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.

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.