Mark Gritter (markgritter) wrote,
Mark Gritter

Learning Swift via Advent Of Code

This year I participated in the Advent of Code contest in a new programming language, Swift. New both to me and objectively! The language is still in active development. I posted my solutions on GitHub: (Technically only 24 programs, because one day I used Wolfram Alpha instead of writing any Swift code.) Because I got started late and used a new language, I wasn't at all competitive for the leaderboard.

The online language guide and other online resources made it pretty easy to get up to speed and start programming, and the increasing complexity of AoC made a good fit, although I feel like I peaked in using new language features about halfway through. One of the downsides of a new language that is still willing to make changes is that many of my searches returned content about Swift 2, not Swift 3. Using Linux was also somewhat of a challenge because a lot of the "getting started" guides are more Xcode-specific. The interactive mode (REPL) helped try language features out, but I never did figure out how to import my modules in order to test my functions interactively.

What I liked about Swift:

Terse but clear syntax. The language removes a lot of cruft from C-style languages. "If" statements don't need parentheses, "switch" statements don't need breaks, closures are easy to write. Type inference means you're getting the benefit of type checking without spending a lot of time writing type names. The range operators ... and ..< beat any C++ or Python equivalent.

C#-style attributes work pretty well too, although I found their usage within the standard library inconsistent: stylistically what was an attribute and what should be a function was not always clear to me.

Swift makes it very easy to distinguish constants ("let") from variables ("var") and I found this pleasing to write, but I'm not sure how helpful it ultimately was. It allows a very powerful 'switch' for enumerating cases, not just value-matching, which I used pretty heavily but probably could have used more.

Type System. There's a lot of things right here. Enumerations are first-class types and can carry extra values. Interface definition via prototypes works pretty well. Structs and Classes make an entity/value distinction you can't get in Python or Java.

The most obvious innovation here is that types can be decorated as "optional": Int becomes Int?, MyClass becomes MyClass?. Optional types permit a 'nil' value and must be explicitly unwrapped (using either an operator or a special conditional form.) The "flatMap" function can be used to simultaneously transform and filter a collection by giving it a function of the form X->Y?, where the 'nil's get discarded, for example:

let program = input.lines.flatMap { Instruction.parse( $0 ) }

(The second part is a closure, and parentheses on the flatMap function call are optional in this common case.)


Buggy implementation, particularly on the Linux port. I filed a couple bugs. One basically prevented me from exploring the regular expression library, since it wasn't usable in the REPL. I also encountered a known issue in type inference that fortunately had a workaround. The CharacterSet class (ported from NSCharacterSet) has a ton of filed bugs and a comment to the effect of "there sure are a lot of these, what are we going to do about it?"

Getting started was hard because the helpful "read a file into a String" function was one that has not yet been ported, so I had to find a (itself buggy) Gist to help me understand how to use Glibc and convert the raw bytes into a String.

Poor tuple support. Tuples work OK at basic stuff, and the ability to do named tuples is promising. Switching on a tuple works pretty well. But compared to Python, forget it. There's no easy way to do list/tuple conversion. Tuples aren't hashable by default, and for AoC I kept wanting to put a tuple as a dictionary key. And the absence of a 3-way zip led to this monstrosity:

return zip( zip( left, center), right ).map { x,y in (x.0,x.1,y) }

There were some libraries I could have downloaded that offered zip3, zip4, zip5, etc. as separate functions.

Whitespace matters for operators. I tried to stay away from this but I got bit a couple times by mistake. You can define new operators, which is cool. But "a + b" and "a +b" are not the same, the latter is a prefix "+". The rules are:

The whitespace around an operator is used to determine whether an operator is used as a prefix operator, a postfix operator, or a binary operator. This behavior is summarized in the following rules:

If an operator has whitespace around both sides or around neither side, it is treated as a binary operator. As an example, the +++ operator in a+++b and a +++ b is treated as a binary operator.

If an operator has whitespace on the left side only, it is treated as a prefix unary operator. As an example, the +++ operator in a +++b is treated as a prefix unary operator.

If an operator has whitespace on the right side only, it is treated as a postfix unary operator. As an example, the +++ operator in a+++ b is treated as a postfix unary operator.

If an operator has no whitespace on the left but is followed immediately by a dot (.), it is treated as a postfix unary operator. As an example, the +++ operator in a+++.b is treated as a postfix unary operator (a+++ .b rather than a +++ .b).

Love/Hate features

Unicode support. They have a very clear explanation of why Unicode-compliant string parsing is hard, and their String and Character classes are clearly written to encourage you to do things correctly. Plus you can use Unicode for identifier names (and not just the boring ones, you can use emoji too.) Unfortunately I couldn't figure out if Emacs really supported this.

The downside is that String manipulation is hard for Advent-of-Code style problems where the input is usually ASCII but you're doing a lot of parsing. Again, maybe if the regexp library were usable for me some of this could have been simpler. But by far the grossest thing I wrote was this code to rotate the letters a-z:

        switch ch {
        case "a"..."z":
            let scalars = String( ch ).unicodeScalars
            let val = scalars[scalars.startIndex].value - scalar_a
            let valInc = scalar_a + ( val + by ) % 26
            return Character( UnicodeScalar( valInc )! )

Often I ended up working with array of characters "[Character]" rather than String, which was a reasonable compromise I think. A friend tried to argue on Twitter that this made you think about the cost of some of the operations you were doing that were expensive on a Unicode string.

Extensions. You can add methods and attributes to any existing class. Even the standard library. "import Foundation" adds a lot of functionality to String and Array. I wrote this which I used over and over:

extension String {
    public var lines : [String] {
        get {
            return self.components( separatedBy: "\n" ).map { $0.trimmingCharacters( in:.whitespaces ) }

".components" is already a String extension added by Foundation (which was confusing at first because I didn't realize I had to "import Foundation" to get all the documented String functions.)

This can also be used to break up your implementation into smaller pieces where a single class definition would be hairy, for example if you want to define some utility classes in a place that "make sense". If you define a new protocol or want to make an existing type hashable, the mechanism works really well.

From a software engineering standpoint, I find this potentially disastrous. I don't entirely understand all the namespace semantics (it's there, and usually unobtrusive) but there doesn't seem to be any way to resolve conflicts in extensions, like there is for class names. I used a hashing library that added a ".md5()" function to String. That's great if there's only one such library in use.

The idiom here is "don't pollute the global namespace" which is good but it seems like you end up polluting every other namespace instead.

Package manager

Mostly this was OK, it "just works". I was easily able to reference external packages. For internal modules, though, I felt like it could do more, and it was hard to figure out the basics of "how do I use a library module in my main program." I ended up with this:

    Target( name: "AocMain", dependencies: ["SimpleFile"] ),
    Target( name: "day1", dependencies: [ "AocMain" ] ),
    Target( name: "day2", dependencies: [ "AocMain" ] ),
    Target( name: "day4", dependencies: [ "AocMain" ] ),

The first target is a library, that depends on another library. The remaining targets are executables. The difference? The day1, day2, day4, ... subdirectories contain a file named "main.swift". Easy to set up yet... I didn't feel very good about it.

It feels like there should be some automatic dependency analysis going on. If I "import Foo" and "Foo" is another library in the same package, shouldn't it just work? And is it linking the external packages with every target? I didn't investigate, but it doesn't feel right to me.

Named Parameters: Swift is showing its Objective-C roots here. If you have a function

doSomething( to:Nail, with:Hammer) -> Woodworking

then you call it like this:

let result = doSomething( to:myNail, with:borrowedHammer )

You can make the named parameters optional, but it's pretty clear this is the desired idiom and sometimes it's used to good effect. But often I end up writing what feels redundant, like:

parse( input:input )
Coordinate( x:x, y:y )

so I don't know if this is my problem and I should make better choices, or a mechanism that could be improved.

The compiler tries to be helpful if you misspell a named parameter, a variable name, or use an overloaded function that's not there. But often it's only helpy, particularly if you have imported Glibc with its ton of symbols.

I enjoyed learning the language but since I'm not doing any IOS programming I doubt I'll stick with it. Probably I will learn Go or Rust next, and keep Python as my prototyping language.
Tags: geek, programming
  • Post a new comment


    default userpic

    Your reply will be screened

    Your IP address will be recorded