Hacker News new | past | comments | ask | show | jobs | submit login
Be wary of functions which take several parameters of the same type (cheney.net)
218 points by ngaut on Sept 26, 2019 | hide | past | favorite | 234 comments



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


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


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.


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.


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.


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

Add exhaustive pattern matches and I was hooked


Levels of indirection are not a “code smell” but they offer many more opportunities for bugs than parameter list order does.


How?


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.


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.

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


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


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


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!


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


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?


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 proper place to pay the dev cost is once in the library, not in all its callers.


Your suggestion is basically Ruby’s `Pathname` class (sans convenient copy method).


That's how Perl's Path::Tiny[1] works, for example:

  path("/tmp/foo.txt")->copy("/tmp/bar.txt");
[1] - https://metacpan.org/pod/Path::Tiny


Amusingly, this has the same ambiguity problem the original post was trying to solve!


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


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.


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.


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.


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.


You can accomplish it in other languages, but will all the other library authors do so? Do the OS libraries do so?


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?


> 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 (optionally) do the same name specified parameters in the more recent versions of c#.


Their documentation still encourages unnamed parameters:

https://docs.microsoft.com/en-us/dotnet/api/system.drawing.r...


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.


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


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.


I would enjoy an explanation of your thinking.


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.


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.


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


outputImageProviderFromBufferWithPixelFormat:pixelsWide:pixelsHigh:baseAddress:bytesPerRow:releaseCallback:releaseContext:colorSpace:shouldColorMatch


  [NSCarpalTunnelSyndromeInducer induceCarpalTunnelSyndromeForProgrammer: ....]


Or, ya know, you use XCode and it autocompletes the function signature for you.


[NSStream dammit]


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.


XCode is really unpleasant as far as IDEs go.


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.


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');


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


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.


Thanks!


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!


or C#:

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


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.


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.


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


for things that have clear direction,

    'foo' `copyTo` 'bar'


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


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


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)


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.


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.


Yup! It's nice and all that IntelliJ ghosts the parameter names in there, but that does nothing for external review, static analysis, etc.


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.


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.


Just be careful not to use mutable objects as your default args.


A great reason to add flake8 to your CI

edit: forgot that flake8 doesn't check for mutable arguments by default – need to add https://github.com/PyCQA/flake8-bugbear


Pylint does


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.


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


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


When it's impossible to avoid ambiguity in the parameters, you can just chose a function name that makes the ordering explicit:

    copy_from_to(file from, file to)


I like this more than being forced to name parameters since that would be too verbose in many applications. But there is a danger that the label of a type becomes less generic.

Vector3(...)

VectorXYZ(...)

The second variant could be weird depending on application.

Still, I see some people argue that positional arguments should be forbidden in newer languages but I would disagree and my main argument would indeed be laziness.


The solution (to me anyway) requires BOTH that parameters must be named, as well as cultivating a culture of creating types aggressively (and, together with that, a language that makes it easy to do this, to avoid the onus of making types leading API developers to make the wrong choice).

Instead of doing:

    downloadFile(String url, String filePath)
which runs into the ordering issue raising in the article, if it was:

    downloadFile(Url from, FilePath to)
you no longer have the issue.

But, having some way to name the parameters, which can take _many_ forms works just as well. It would help a lot if the language can enforce that you must do this:

named params:

    downloadFile(from = "https://something/foo.txt", to = "/some/path")
objC style where the function name is split up:

    downloadFileFrom: "https://something/foo.txt" to: "/some/path";
same as above, but with fluent-style intermediates:

    download().from("https://something/foo.txt").to("/some/path");
pass an object (Javascript):

    downloadFile({from: "https://something/foo.txt", to: "/some/path");
these all work just as well. And nevertheless, this:

    download(server, file)
is cleaner than all of the previous attempts, and allows passing these concepts around helper methods and the like. So, _IF_ it makes sense to change the types of the arguments so that you no longer have the same type for different arguments, it seems preferable to me.

However, in the case of CopyFile, to take that to its logical conclusion, you'd have a separate type to represent a 'from file' and a 'to file', but that seems too much of a stretch: The notion of a file fundamentally doesn't have a 'direction' concept built into it; that makes sense only for a copy or move API. Making separate types to represent the tuple of [direction, filepath] seems like a hacky way to introduce named parameters.

So, have both.


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.


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


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


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.


Nice, very clever!


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


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?


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


That doesn't scale past two arguments.


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.


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)


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.


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.


There was an experimental language I can't remember where arguments were only named by their type by default, rather than having identifiers. This implicitly discourages having multiple parameters of the same type. After I fully processed the idea, I came to like it quite a bit.


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.


While it's not a perfect example or solution, this illustrate a fairly common issue with modern code. He also mentions too many parameters. Other commonly frown upon constructs include unclear names (java "filter" or "compareTo" easily come to mind), boolean parameters (One should use named enums instead). Some add to the list out parameters (passing a to-be-modified list as a parameter), functions returning their modified input parameter, even though they certainly have their usefulness (like being able to handle memory management outside of the callee). I'd personally like to add String parameters to the list, but modern language make such restrictions often too cumbersome.

I the given example, I'd love to see a language with optional named parameters. For the function copy(String fromFileName, String toFileName), instead of calling copy (file!, file2), one would have the possibility of calling copy(from:file1, to:file2)


Your last point is something that I really like about the Swift language. All parameters have names that are mandatory when calling the function, so in Swift you would be required to call copyFile(from: file1, to: file2). [0]

0: https://docs.swift.org/swift-book/LanguageGuide/Functions.ht...


In C# you can optionaly name parameters when you call a method, sometime this is mendatory when you want to set the second(or more!) optional argument, but you don't want to set the first optional argument.


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


That's a nice idea. Also, while I love C++, I'm really frustrated that there are no native named arguments :(

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!


IMO this is totally solved with named parameters. Nobody’s gonna mess up CopyFile(from:a, to:b). Nobody.


I love Golang but I often get "copy" wrong because it's the opposite of UNIX: copy(dst, src) -- what?!

https://golang.org/pkg/builtin/#copy



With Polymorphic Identifiers:

   file:bar := file:foo.
Let functions compute return values, use assignment if you want to move data.

References:

https://dl.acm.org/citation.cfm?id=2508169

https://www.hpi.uni-potsdam.de/hirschfeld/publications/media...


Most of these have conventions. For copy, move, etc the convention is from target, to destination. And an IDE will show you the parameter names. If you still get it wrong, there is also no guarantee that you would assign the correct type either. eg. mixing up foo and bar in {from: foo, to: bar} or foo:From, bar:To Also if there is any speculation you should always read the source code of the function you are calling, or read the documentation, and try the function in a REPL to see how it behaves. There is a line where over- verbosity will have the opposite effect eg. making the code harder/slower to comprehend. So keep it clean and simple. If people get it wrong a lot, make the function figure out the parameters so that it doesn't matter what order you pass them in. And have the function throw a friendly human readable error if you get the arguments wrong! JavaScript example:

    function copy(from, to, overwrite, cb) {
        if(typeof overwrite == "function" && cb == undefined) {
            cb = overwrite;
            overwrite = false;
        }
        if(from == undefined) throw new Error('First argument "from" not specified!');
        if(to == undefined) throw new Error('Second argument "to" not specified!');
        if(!exist(from)) throw new Error("from=" + from + " does not exist!");
        if(exist(to) && !overwrite) throw new Error("to=" + to + " already exist!");
        ...
    }


* Not all users have IDEs with all that much help (vim, emacs users come to mind), and not all platforms have this ability (consider for example writing serverless functions in an online IDE), and sometimes you do code reviews from an online platform or from your phone, and sometimes you just kinda pull up a piece of code in notepad, and I think you should be able to tell that the code is obviously correct in all of those cases.

* In C, the convention is "move(destination, source)". See memcpy, snprintf, strftime, others, but not all functions follow this convention. PHP is known for having lots of functions in standard library with differing conventions.

* It's easier to tell that "move(from=X, to=Y)" is correct than "move(X, Y)" though?

* I agree with the over-verbosity, but your last point that "annotating your arguments makes your code over-verbose and harder/slower to comprehend; instead, put all these guards and magic in the definition of the function" - isn't that also making your code harder/slower to comprehend, but this time you're putting the mindwork in understanding the function instead?


An IDE is sometimes used as band-aid for bad design. I use an IDE for convenience, but try to make my code not depend on it. It's said that the time it takes to type the code is a very small part of software development. Yet programmers are lazy and would write the short-hand if one is available. I think it's best to learn one language, and one code-base well - if you want to be productive. (or maybe become bored) There is a trend in software, that things get dumbed down, and locked down. It shouldn't take 15 years to become productive, but if you aim for one month or less, you would have to sacrifice a lot.


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.


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.


There's a lot of hate in the Go community for languages like Haskell, and a lot of pride in Go for "keeping it simple" and "staying out of your way."

Yet if you watch Go conference videos or read Go blogs, you'll see quite a few idioms like this one, which boil down to just wanting to use real types for things, instead of layering semantic meaning onto strings or ints or nils, and keeping that meaning in your head (or in the docs).


Does anyone else feel this is... ridiculous? In that “at some point, you do have to start paying attention to the code you’re writing” way?


I feel the same way sometimes too. One thing to consider though is that when the overall complexity of a project increases these little things do add up.


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


My suggestion of a better solution would be better tools. For instance, when viewing or editing the code, a call to CopyFile should appear something like

   CopyFile([from] 'presentation.md', [to] '/home/presenter')
with the "[from]" and "[to]" parts not being actual tokens that are part of the source file, but annotations that appear to assist the developer. They would appear as little badges, non-editable or selectable, with distinctions in their display to make it clear they are UI elements rather than part of the source code.


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


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.


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


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.


Using key-value pairs? (Julia) `copy(src=>dest)` The advantage of that notation in this case is that expresses an explicit non-conmutability of the arguments.


The Unix/Linux command line is unfortunate like this; the cp/mv/ln commands would be better off with the destination specified specially. For instance gnu cp allows the syntax

   cp -t <dir> <source...>
Which is good because it makes it similar to most other commands.

It would be unfortunate to mandate it though; it would be unfortunate for an `option` to be mandatory.


It also allows you to use xargs to pipe a list of files in.


> You can’t tell without consulting the documentation.

Referring to the documentation is what you should be doing the first time you use a function, right? Most IDEs now have handy tool-tips to display parameter details.

I use named parameters when available, but it's no big deal to pass parameters to a function. This is why we write tests.


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.


Someone should introduce a language feature - maybe a decorator - where arguments have to be called by name.

  @mustnameparams
  copy(str from, str to)

  copy(from = "blah", to = "hrg")
  > works

  copy("blah", "hrg")
  > throws error


you mean something like def pythonDoesThis(self, *, from='':AnyStr, to='':AnyStr)->AnyStr:


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.


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.


  Window XCreateWindow(Display *display, Window parent, int x, int y, unsigned int width, unsigned int height, unsigned int border_width, int depth, unsigned int class, Visual *visual, unsigned long valuemask, XSetWindowAttributes *attributes);


I would be wary of functions that take several parameters regardless if they are of the same type -- at that point you're already going to have to look into the documentation.


This is one example of why working in a higher-level language, if you can for a given task, makes life easier.

C#:

PrintOrderDetails(orderNum: 31, productName: "Red Mug", sellerName: "Gift Shop");


Your junior colleague would still do PrintOrderDetails(1234, "SomeSeller", "SomeProduct");

And the compiler wouldn't notice.

You could uused a Roslyn analyzer that gives a warning "methods with consecutive args of the same type should use named params" I guess. Or a warning could be made at the declaring site "Don't declare methods with consecutive args of the same type".


Or just use keyword arguments in Python. I am debugging someone else's code today and it make it so much easier when you have named keyword arguments - which he doesn't.

:(


I constantly tag primitives in scala. It is boilerplatey even with shapeless, but worth it. Tagging classes might be a bit too much though


in kotlin, named parameters are a good defense against this.

They help in many cases. Mixing named and default parameters in a method, modifying an API without breaking its usage, etc.

It is slightly more verbose, but I tend to use them a lot. Basically everywhere unless the params are obvious (max(a;b) is a good example)


> Basically everywhere unless the params are obvious (max(a;b) is a good example)

This isn't so much "obvious" as "irrelevant"; the arguments to max are all exactly equivalent -- their order can't affect the result.


Expanding on the point a little further, it would be normal from certain mathematical perspectives to see max as a unary function, mapping an unordered (multi)set to the set's maximum element. With one argument, order can't matter because only one order is possible.


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

    thing(dest=a, src=b


TLDR: Be wary of noncommutative algebras!

In particular, if you have a noncommutative operator, do not denote it with a symbol that is symmetric along its vertical axis.


In math, you have: matrix multiplication, vector cross product, ... So where does this idea originate?


In linear algebra, you typically use juxtaposition to represent multiplication, rather than explicitly with a symbol. For some reason, this actually seems to capture noncommutativity better than a symmetric symbol.

Another example would be string concatenation, which people often debate the merits of using a + to represent.


I guess it originated from my synthesis of the article.

The article gives an example: copy() bad, copyFrom() good. The single word "copy" doesn't imply which side is which, it is kinda "symmetric". However "copyFrom" does show the side, it is kinda "asymmetric". I just considered symbols instead of words as method names. (After all, if we are to make things shorter.. TLDR is a double joke here.)

And yes, you're correct, in mathematics, this is often not the rule. It's not even a rule in Haskell. But perhaps it could be a rule, at least for new operators?


Something about architecture astronauts comes to mind...


For me Intellisense pretty much solves this problem


I don't get why this got upvoted.



Hah! I did not know that term. At least I learned something related to the post and its comment section! :)


For a second, based on the domain, I wondered if Dick Cheney had re-invented himself as a developer.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: