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') 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')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 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.
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!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::Tiny [[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.
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.
Copy.from('foo').to('bar').execute();
Because of variations like these: Copy.from('foo').to('bar').owner('me');
Copy.to('bar').from('foo'); 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.
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.
```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!
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)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).
'foo' `copyTo` 'bar' 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")