Be wary of functions which take several parameters of the same type
dave.cheney.net
dave.cheney.net
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') shutil.copy(src='foo', dst='bar')
That's straight out of stdlib, btw.The parent post outlines many techniques, including this. Some are more apt than others depending on your language of choice.
I would argue that there's another option of passing a strongly typed struct for args, and that the JavaScript and Ruby solution of dicts falls between this and kwargs.
I'm fond of properly designed fluent APIs, myself.
While on the subject of homogenous argument types, especially bad are the APIs that not only accept the same types, but have order semantics that differ between functions within the same library. PHP used to be a notoriously bad example of this with respect to their needle and haystack search parameters. (I think they fixed it?)
The "JS solution" for example is not a solution, but only a convention: the actual interface is just expecting an object, and there is no explicit check on the validity of such object or its contents. That is nowhere near "strongly typed", if anything it's weaker than average.
Well if it uses de-structuring syntax, then it can be type-checked.
interface CopyArgs {
src: string;
dst: string;
}
function copy(args: CopyArgs) {
exec(["cp", args.src, args.dst])
}No, they put out a conf talk that stated that there _was_ a logic to it, you just needed it to be explained to you.
But nobody talks about how nonsensical C function names are.
[[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.one can still make a hot mess of 1000-wide lines, bad variable names, and various other forms of spaghetti cowboy-ism.
My aim isn't to argue that forced named args will cure all that ails you, just to share that they "feel" quite different from optional named args, and that on the balance I felt they were a net positive.
One of the first things about Swift that I fell in love with was the difference between things like DrawRect(10, 20, 30, 40) in languages like C#, and DrawRect(at: origin, width: 30, height: 40) in Swift.
One of the most pernicious myths of software engineering is that it happens.
If I see a chain of numbers like 1, 2, 3, 4 my brain has to recall the order of parameters or look it up.
If I'm looking for rectangles that should be wider than they are tall, I have to keep thinking "the third number should be higher than the fourth number"
Oh and I can't just Cmd+F "width:" or "height:" either.
https://docs.microsoft.com/en-us/dotnet/api/system.drawing.r...
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.function DrawRect(options) {...
Unless you're immediately destructuring it or whatever. I don't think it's _that_ straightforward to compare the two, unless I'm thinking of the wrong languages here.
It will be checked compile time that all keys are provided (unless dict comes from serialized json or something).
Well typing is one good reason, maps usually are not statically typed with key:value pairs. That's more of a class / struct thing. And then the verbosity increases quite a bit.
I'd rather have syntax-level support (works with ctrl+f, works outside IDEs, works in IDE when auto-analysis is broken), but if IDE support is what I can get, I'll take it.
[NSCarpalTunnelSyndromeInducer induceCarpalTunnelSyndromeForProgrammer: ....] [NSEyeStrainInducer induceEyeStrainForProgrammer: ....]
Unless XCode can read the signature for me in addition to writing itHonestly thinking back on Java there were a lot of design patterns and code (sometimes generated) that could've been avoided with e.g. named function parameters.
And if you want, you can make arguments keyword-only:
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)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.
func fn1(myLabel1 myParam1: int, myLabel2 myParam2: int) {
}
fn1(myLabel1: 123, myLabel2: 456)
func fn2(_ myParam1Omitted: int, myLabel2 myParam2: int) {
}
fn2(123, myParam2: 456)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.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.
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).This is Wrong, with a capital W.
The file copy API is fundamentally different to reading and and then writing the contents of a file.
The "copy" system call on most platforms also:
- Copies metadata such as attributes.
- Copies alternate data streams.
- Preserves advanced features such as sparse files, compression, encryption, BTRFS/ReFS/ZFS integrity settings.
- Can copy externally archived files without restoring them.
- Can copy files server-to-server without transferring the data to the client computer.
- Copies block-by-block instead of all at once, allowing files to be copied that do not fit into memory.
- Copies asynchronously so that reads and writes are overlapped
- Etc...
The code above could, however, just deal with references to some file handle and not actually read or write the contents (though ReadFile() or WriteFile() would be less-than-ideal names for such functionality).
You're also focusing too much on the example, and missing the greater problem. There are other cases that fit the original problem, such a sending an email (who is the sender and who is the receiver?)
My point is that the high-level concept is not always appropriate. Sometimes it's overkill. Sometimes it's outright wrong.
> sending an email (who is the sender and who is the receiver?)
The sender is already encoded in the message itself in that case. The receiver is specified as a specific SMTP message, last I checked. So this problem wouldn't really apply here.
Even if it did, though, the separated functions would still be useful here:
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")CopyFile(src: "foo", dest: "bar");
It would be better to have a foolproof API, but obviously it can't change now.
new FileInfo(srcPath).CopyTo(dstPath)
On the other hand, it also has the more error-prone File.Copy(sourceFileName,destFileName); 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.If you can't keep track of your variable names, then using keyword arguments, like others here talk about, won't help much, either.
I'd definitely agree with you that the not having named arguments is worse than your method chained example.
To me named parameters seem to be on a par with the chained OO api.
File.Copy(a,b)
and File.Copy(sourceFileName: a, destFileName: b)
And confusingly, also as File.Copy(destFileName: b, sourceFileName: a)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.Why is chaining necessary for this? On-instance tab-autocomplete list-all on-tab accomplishes the same thing.
I have no idea what you just said.
(While editing code, position the cursor) on (a class) instance (and press) tab (to swith to) autocomplete (mode and) list all (members of that instance by clicking) on tab (again).
(e.g. foo.[TAB])
can list all possible operations available on that instance -- in other words, you don't need a fluent interface for this.
Is that better in other language runtimes/debuggers?
```c# var file = File("source"); file.CopyTo("target"); ```
(apologies if this is formatted badly; no clue what HN supports here, and I'm on mobile)
path("/tmp/foo.txt")->copy("/tmp/bar.txt");
[1] - https://metacpan.org/pod/Path::TinyIn 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 fromYou could "force" parameter naming, and it would be easier.
Types are good but having 10 types of string doesn't make sense.
Not if the strings are clearly distinct (like a name being different from an e-mail address).
It really does make sense. Types like string, int, float, etc. are poorly defined for most things they represent. There's no context and are just one step away from being dynamic, like values in a dynamic language.
By enforcing a contextual contract (perhaps with additional constructors that constrain the value) you leverage the type-system to do the hard work of checking for stupid mistakes. On large code-bases this reduction in cognitive complexity makes a whole class of bugs go away.
So, 100s of types of string could make sense, it really does depend how many types of thing you have which are distinct and can be represented as a string.
But once they're defined, they're not strings any more, they have an internal state which is a string, yes, but their type is not a string.
Of course none of the above are unsolvable problems. I've never seen anything that actually makes it easy, but I won't claim to have seen everything.
It's not. It's 10 types.
Do you? Or do you need just the operations for the things that are relevant to the type at hand? Limiting a broad API to the needed interface for the type you're modeling is a GOOD thing.
Add exhaustive pattern matches and I was hooked
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)What is the advantage here? As I would prefer to have the authors original problem than this.
I was merely trying to show how to quickly emulate the Haskell example with less code in Python. Both the Haskell approach and asserting on types in Python seem like wastes of time to me personally, and I’d rather just have copyFile by itself and consenting adults can pass the args they want to pass. Use unit tests to make sure you’re not passing paths the wrong way.
Also, what’s with the attitude? “You know how to complicate something very simple” (from ~15 lines of Python?) — geez, you must be a joy to work with.
Basically from a You Aren’t Gonna Need It point of view, doing it in tests lets you minimally address the real use case with much less risk of premature or incorrect abstraction.
In my experience, premature abstraction is one of the worst problems in business software. To contrast two far ends of the spectrum, minimalistic patchworks of gross hacking are asymmetrically way better than overhead of premature or incorrect abstractions.
copy(source=file1, destination=file2)
and be done with it. Or am I missing something? 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. 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.Any solution could be screwed up on purpose if you’re imagining someone at an interpreter prompt getting the arguments wrong or omitting unit tests that verify a certain file is used as source and a different file as dest... which is why this kind of objection just totally doesn’t matter.
Not saying this makes static typing better or anything, just pointing out that “easy to spot in code review” is massively different from “absolute mathematical proof this problem isn’t affecting me.”
I’d argue you rarely care about such compiler proofs in real software development, but that’s beside the point if you assume a static typing paradigm has already been chosen.
Using inheritance and metaprogramming to enforce how and when a given instance or class object satisfies the contract of some interface is a core, fundamental, first-class aspect of Python.
Just because you don’t need to use it very often doesn’t mean “it’s the wrong choice for Python” or anything like that at all.
It is a thing Python goes way out of its way to enthusiastically support, so therefore it is absolutely a Pythonic way to solve problems.
No. Far from it. First of all, what is File in your example? a class? an object? and what does File().Source() return? an object? what does that object represent?
If you want it to be OO, you have a File object, to which you send a "copy" message. Depending on how strictly OO you want it to be, it then returns the new file object (or a reference to it).
Copy.from('foo').to('bar').execute();
Because of variations like these: Copy.from('foo').to('bar').owner('me');
Copy.to('bar').from('foo'); 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);
}Stop! Put down the editor and step away from the compiler, please. No one wants to get hurt.
A problem I have with the types solution is that, although it's easy to tell if the code is wrong, it's often a bit of a puzzle in my experience to figure out how to write it in the first place: "Hmm, this takes a Foo and a Bar. Can I construct a Foo from a string? No, but I can construct it from a Baz or a Quux. Can I construct a Baz from a string? No, just an int. How about a Quux? Yeah, there we go. Now, how about that Bar?". And I've seen other people have that problem too, especially novice programmers. I'm not a novice but it's still a problem for me in a new type-heavy API.
I realized I was creating that problem in Yeso, and I couldn't figure out how to simplify the API, so in addition to starting the documentation with complete examples, I made a graph with graphviz that shows the type structure of the API: https://gitlab.com/kragen/bubbleos/blob/master/yeso/README.m...
I don't have that problem with named-parameter interfaces (I just look at the list of methods and parameter lists), and maybe I could run into it with fluent interfaces in theory, but in practice that hasn't been much of a problem in my experience.
The article author was writing in Golang, which doesn't have named parameters, but in languages like Python, Lua, Clojure, or, as you say JS, named parameters are straightforward. In Lua it's even simpler than in JS:
copy {from='presentation.md', to='/tmp/backup'}
I would never claim that Lua isn't bug-prone, though! 'foo' `copyTo` 'bar'```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!
def foo(x=None, y=None):
...
foo(x=1, y=2)
foo(1, y=2)
foo(1, 2) 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.edit: forgot that flake8 doesn't check for mutable arguments by default – need to add https://github.com/PyCQA/flake8-bugbear
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.
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.
copy_from_to(file from, file to)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.
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.
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.Developing C# in rider if you call `foo(a,b)` then it'll display as `foo(from: a, to: b)` in the IDE.
I write my libraries like this because i want to beat my users into being explicit -- thus the "a little autocratic, but.." caveat.
(Those that don't will need to pass in named tuples, or structs, or similar data structures.)
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).
type CopyFile string
func (src CopyFile) To(dest string) error {
// copy file here
}
func main() {
CopyFile("presentation.md").To("/tmp/backup")
} Source("presentation.md").CopyTo("/tmp/backup")
while scoring slightly fewer slickness points, wins overall imo because it remains accurate.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...
myObject.doThing(a).toAthing(b)
Though that requires a good amount of boilerplate on the part of the library author.Mary.give(John, apple)
a + b -- '(+)' function used infix
(+) a b -- '(+)' function used prefix
min a b -- 'min' function used prefix
a `min` b -- 'min' function used infixĉu A estas pli granda ol B? (S-V-O) (lit. "whether A is more big than B?")
ĉu A pli granda ol B estas? (S-O-V)
ĉu estas A pli granda ol B? (V-O-S)
and never "estas pli granda ol A B" as you suggest, but also never avoiding any indication that it's a question.
From a natural language side, it feels very contrived and arbitrary to have one hill measure whether it is bigger than another hill, instead of having an observer measure both 'from the outside'. That suggests the imbalance in syntax between a. and (b) is strange. a > b is more balanced and looks more like an outsider doing the comparison.
The reason functions with these names (and comparator operators) return booleans is precisely to make the code read like natural language. Look at the C syntax:
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.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.
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)
0: https://docs.swift.org/swift-book/LanguageGuide/Functions.ht...
I use this technic to disambiguate all kind of types (source/destination gpu/cpu pointers for instance) and correctly dispatch everything at compile time.
The compiler becomes your best friend then. :)
However, after your comment, I decided to spend a few minutes on the topic, and came up with these links:
https://www.fluentcpp.com/2016/12/08/strong-types-for-strong...
https://www.fluentcpp.com/2018/12/14/named-arguments-cpp/
I am now looking forward to refactor some of our code base to make use of this. Awesome and thanks for the nudge!
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...
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!");
...
}* 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?
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.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.
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).
For example, changing an ambiguous foo(float x, float y) into foo(distance x, weight y) .
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.Yeah. Mind your docs.
Languages and APIs designed like this and get you crippled tools that simply do not allow you to express about half the shit you're gonna need to do. That generates another layer of indirection, abstractions, and workarounds to break through the "we think for you" for when what "they thought for you" wasn't what you was actually thunking.
There's a place for languages that don't allow you to shoot yourself in the foot or other member of your choice, I'm sure. I don't want to go there.
What problems would you see arising (either directly or indirectly) from the specific suggestion in the article, for example?
This isn't about removing flexibility, it's about helping people to avoid making easy mistakes that could be prevented.
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...
Obviously F#’s first class support for UoM is better than my implementation, but for all intents and purposes the behaviour is the same.
[1] https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSha...
I have seen your lib before - nice work.
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.
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.
async function copyFile({
source,
dest,
}: {source:string, dest:string}): Promise<void> {
// ...
}
await copyFile({source: 'presentation.md', dest: '/tmp/backup'}); @mustnameparams
copy(str from, str to)
copy(from = "blah", to = "hrg")
> works
copy("blah", "hrg")
> throws errorSome 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.
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);C#:
PrintOrderDetails(orderNum: 31, productName: "Red Mug", sellerName: "Gift Shop");
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".
:(
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)
This isn't so much "obvious" as "irrelevant"; the arguments to max are all exactly equivalent -- their order can't affect the result.
thing(dest=a, src=bIn particular, if you have a noncommutative operator, do not denote it with a symbol that is symmetric along its vertical axis.
Another example would be string concatenation, which people often debate the merits of using a + to represent.
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?