Hacker News new | comments | ask | show | jobs | submit login
Writing custom tools with Swift (paul-samuels.com)
96 points by ingve 33 days ago | hide | past | web | favorite | 41 comments



Swift tries to be a jack of all trades, but I think as a scripting language (as in this example) it falls short. Having to start a run loop, write asynchronous callbacks (completion handlers), and implement custom JSON decoders just to make a web request is introducing a ton of complexity that might make sense in an event-driven interactive GUI application, but not so much in a quick shell script.

Swift tools might be a good choice for use cases where you need to integrate with an existing Swift project, or if you need lower level APIs.

In this example, a Bash script would have been almost done by the time you worked out the `curl | jq` command. But in other similar cases I would suggest Python Requests, which will take perhaps 10% as much code and avoid issues of Linux compatibility and mistakes like forgetting to call `resume()` on your download task or `exit()` in some branch (there are five calls just to keep the program from looping forever).

That said, I think this blog post is very informative and well-made for a beginner interested in talking to a web-based JSON API from Swift.


Swift's APIs tend on the verbose side, but they can be wrapped up into much more concise versions. Since few people have been using Swift in this way, those wrappers haven't really appeared yet (and because Swift packaging is still in such poor shape) but it will come - just like the "requests" library made http so much more concise and easy in Python.

The obvious question, of course, is: why bother? The answer is that this code runs at about the same speed as C. So if you're doing data munging of a big file, this is a really interesting approach. For data scientists, I think there is a lot of opportunity here.


Oh BTW there's already something for Swift inspired by python's Requests lib: https://github.com/JustHTTP/Just/blob/master/README.md


Making libraries concise is key - if you're trying to "write bash" in Swift, then being able to glue things together quickly and easily (as if you were piping to/from curl, grep, etc) is important.


But then you might as well use Crystal instead, which already have scripting language usability while retaining the performance.

I like the idea of swift and I want to like it, but everytime I look at APIs it seems they dropped the ball.


>But then you might as well use Crystal instead, which already have scripting language usability while retaining the performance.

Without the libraries, documentation, maturity, and major support.


Well, assuming the plan is to run the program on something that is a server, the same basically holds true for swift as well. There simply isn't enough people using it for server stuff yet for it to have become battle proofed.

But yes, if the plan is to run it locally on a mac, then your objection certainly hold.


Excuse me but swift libraries plain suck, because of the bad package management. Documentation is also poor and mostly “look at the example, tests, or source”.


Documentation is poor? There is lots of documentation for every iOS/Mac class and feature on developer.apple.com, and for the language and standard library, and dozens of books (several of them made by Apple and free). Plus tons of third party blogs and videos on Swift programming.

That's more than anything that can be said for Crystal documentation.

And bad package management or not (I don't see much problem with it -- and you have several options to chose from, cocoa pods, carthage, etc), there are tons more libraries than Crystal has again.


Oh sure the standard lib is well documented, I thought we were talking about 3rd party libs.

> And bad package management or not (I don't see much problem with it -- and you have several options to chose from, cocoa pods, carthage, etc)

That's the problem. It should be SPM, period. But few libs use that, and some still don't work on linux and you don't realize until you try to build. Carthage is slow, hard to integrate with xcode project and CLI. CocoaPods, I guess it was super useful before swift, but now it seems like it can only work with xcode projects.


>Swift tries to be a jack of all trades, but I think as a scripting language (as in this example) it falls short. Having to start a run loop, write asynchronous callbacks (completion handlers), and implement custom JSON decoders just to make a web request is introducing a ton of complexity that might make sense in an event-driven interactive GUI application, but not so much in a quick shell script.

Well, you don't need to do any of those.

You could make a synchronous web request.

And you could decode JSON into a representation, instead of pre-creating a struct with the exact result.


into a representation -> into a generic representation


A generic representation I guess means primitives, dictionaries and arrays. Stringly typed.


No -- "primitives, dictionaries, and arrays" typed, which is different from "stringly typed", and representative of what JSON actually is.

JSON doesn't represent class hierarchies, but plain nested JS objects. And the types it supports are just dictionary, array, string, boolean, and float.

So having all the ceremony in Swift (or similar in Go, etc) to parse into predefined structs is not really idiomatic to JSON.

You can have all of those in the generic representation -- and this is exactly how any dynamic language does it (JS, Python, PHP, etc.) -- and any statically typed language when it wants to read arbitrary JSON.


JSON does not represent class hierarchies, but neither dictionaries. It represent structures, 99% of the time the exact shape and format is public. Parsing JSON into predefined structs is indeed very idiomatic.

A dictionary is a key/value map, period. If you want to use that, because you don't want to do the encode/decode ceremony, then you are doing stringly-typed code in a statically typed language. This is not bad per-se, as you say dynamic languages always do this, but you should recognize what you are doing.


>A dictionary is a key/value map, period. If you want to use that, because you don't want to do the encode/decode ceremony, then you are doing stringly-typed code in a statically typed language.

It's not stringy-typed since all the scalars have the proper type as they did on the "wire". In JSON you have string, booleans, floats, objects, arrays, and null.

And JSON is not a general serialization format (e.g. all object keys are strings, all numeric values are floats), so this "encode/decode" ceremony just introduces a layer that isn't there to begin with (and would fail without custom decoders for things like dates and so on).

In general the decode is really a mapping for convenience and schema validation, than an actual decode of what the JSON contains (since JSON knows nothing about those structs as structs).


i never referred to values, but the structure. hammering json into a dict is stringly-typed.

it is stringly typed when you do foo[“bar”][“baz”] etc. foo.bar.baz would be typesafe.


I totally agree. I was curious how much typing overhead there is in the swift version so ported that blog post's code into nodejs. In node, the whole thing comes in under 30 lines with comments. (Compared with 86 lines in the final version from that blog post).

https://gist.github.com/josephg/469ddbe71b7c515247b0db801325...

The reality is you always have to do more work to interact with JSON data from a static language because you need to tell the compiler what type everything will be. The benefit you get from doing that can pay off in larger projects, but for quick and dirty scripts its a lot of wasted effort.


As developer that does not know how to write Bash scripts at all but is well versed in Swift, if I have to write such a tool, I'd rather use such "suboptimal" option than have to learn a new language.

It also has the advantage that, if later this solution needs to grow past a simple command line tool, I can just grab the Swift code from the script, put it in an Xcode project, and make a Mac app out of it.


It takes a while, like anything, to get comfortable with shell scripts, but it's absolutely a tool that every programmer should have basic familiarity with. It's universal glue.

And in the domains where shell is a good choice, it's a far far better choice than Swift. Maybe that will change, but I doubt it.

I've implemented a couple of medium-small tools over the last two years and as an exercise did them in both shell and Swift. The shell versions are simpler, more maintainable, and more robust than the Swift versions. Swift's verbose "correctness" is not an asset in the domain(s) where scripting is useful. Gluing programs together is one. Frankly, text processing is another. An invocation of sed with a simple regex becomes a monstrosity when translated to Swift. The less code you write, the less you have to maintain.


Agreed, along with basic familiarity with vi/vim this is almost required knowledge if you're deploying to or working on a system other than Windows or MacOS.


It definitely applies to Mac as well; it's got a full complement of BSD command line goodies.


> An invocation of sed with a simple regex becomes a monstrosity when translated to Swift.

This is because Swift’s regex support is overly verbose, being lifted straight from Objective-C. It’s not an inherent problem when the language.


Swift as a (shell) scripting language has been terrible for me. To get us to Swift shell scripting, I think what's needed is the "syntax" for calling out to other executables directly instead of wrapping it in a task. Of course, plumbing follows immediately and I don't know if we can keep the shell plumbing syntax in a Swift environment.



That should be doable with a library, right? Is there any need for language syntax for this?


The tricky part is that it is impossible to import any libraries in a script except:

- The official libraries that ship with Swift

- If you create a full-blown Swift package manager project and compile an executable. But this doesn't sound like a script anymore

- If you use John Sundell's Marathon [https://github.com/JohnSundell/Marathon] however it adds additional complexity around the writing of scripts

I.e. none of the solutions are as simple as writing a bash script

Edit: Just saw that Max Howell (creator of Homebrew) released this today https://github.com/mxcl/swift-sh This absolutely solves the issues. Fantastic.


swift-sh definitely bears looking into, thanks for that!


If all you're doing is calling external processes, well, it's hard to beat the shell piping syntax.

The problem is of course that as the complexity of the script grows, the absolute dreadfulness of actual programming in the shell rears its ugly head. I haven't really found a good compromise here myself.


You can do piping in Swift, like so: http://noze.io/noze4nonnode/


You make a good point wrt complexity. A project to convert shell scripts into (even naïve) Swift code (form compiling) might be useful.


For the plumbing, maybe a library (of operator overloads) is all that's needed. For calling out to executables like a script (i.e. just put the exe name inline as if it were another symbol), you'd need first-class language support.


I don't see that working very well. If you ever made a typo in a function name, the compiler would have to assume that you are referring to an external tool that would be available at runtime.

Swift gives the programmer a lot of flexibility in overriding operators, though, so one could probably make a decent DSL that approximates shell syntax.


For scripts, you're not compiling an executable that you'll run later, you're having Swift execute the code "now" so "available at runtime" becomes "immadiately after you parse the code."


I'm bullish on the trend(?) to implement tools in languages like Rust, Go (and maybe Swift like in this article, although I'm not familiar with it, and it seems mostly unheard-of here in Linuxland). Being able to just deploy a binary with minimal dependencies (yes, I did read the article, but I'm assuming with swift you can also create a static binary) makes it a lot easier to get started than requiring the user to setup some virtualenv for python.

And while not relevant in this particular case, native binaries are fast, and launch quickly as well.


> I'm assuming with swift you can also create a static binary

This is not possible yet, unfortunately: https://bugs.swift.org/browse/SR-648


If you've got a mature, all-encompassing build flow (hierarchical Makefiles, Bazel, etc), then ensuring a required tool is up to date after a pull is very straightforward. The weaker your build system, the more you really depend on tools being interpreted scripts so they "just work".


I like it, being able to start with a shell script and then evolve the tool to a static binary is pretty neat, I do that all the time with ActionScript

I don't think it's trying to be a "jack of all trades", sure Bash can do plenty but when you reach few hundred lines of Bash and start to fight against the syntax to do simple things well...

The shebang line is there to be used, you can use sh, bash, etc. but there are other citizen like perl, php, python, etc. so why not swift or anything else if it help you build quickly command-line tools?


Depending on your usage, a "script" done in a language like Swift, Go, or Haskell will probably take a little longer to put together than a bash script. Bash merely glues together generally-available command-line tools with pipes and minimal error handling, whereas the other languages typically require you to put forth some energy to use their library and validating outputs.

If all you care about is ticking a box in the shortest amount of time, I've found that small scripts are best done in bash whereas longer "scripts" would benefit from a safer language utilized in a JIT compilation paradigm (shebang for Swift or Haskell, `go run`, etc).


Fsharp is a much better language for scripting than swift will ever be. F# with the pipe operator used judiciously makes reading it a joy.


[flagged]


I generally agree, but sometimes it's through pushing the boundaries that we begin to realise what the shortcomings are not only in one domain but potentially in others. This might not be a great move forward for shell scripting generally, but these sorts of things could be wonderful steps in terms of progress for the Swift language itself and possibly for Apple's tooling (some of which still relies on old versions of Python and Ruby).




Applications are open for YC Summer 2019

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

Search: