Hacker Rank in Swift - Reuse Code
[OUTDATED]
Reuse Swift IO code for multiple HackerRank assignments.
If you have read this article, you have probably noticed that code to read from stdin must be copied to each assignment file. Even though you need to copy-paste entire solution to HackerRank web site, this goes against DRY principle. In this post I’ll explain how you can keep all stdin code in one file and use it to run code for assignments.
Start by grabbing Swift code, you can use this file for example. Put it in a file, name it StdIO.swift
and put it in the root of your HackerRank folder alongside the makefile from this post (you’ll need that makefile later on).
Swift Module
For next step you should have 2 files. A StdIO.swift
file created earlier and, say, solve-me-first.swift
for the warmup assignment.
let a: Int = readLn()
let b: Int = readLn()
println(a + b)
If you try to run solve-me-first.swift
with makefile created in the article mentioned before, you’ll get nowhere. Swift compiler has no idea where to look for readLn
methods, so it can’t interpret this script file alone. We have to build a Swift Module for StdIO.swift
and then link it with our main Swift file.
Check out this and this link to find out more about building a Swift module. I’ll just provide a summary here. Once a Swift Module for StdIO.swift
is built, it will consist of 3 files:
StdIO.swiftmodule
- public interface and definitions. An analogue of header files from Objective-C world.StdIO.swiftdoc
- documentation.libStdIO.dylib
- a shared (aka dynamic) library. That’s actually a binary that has all all your code compiled, much alike dynamic library for C, C++, Objective-C and so on. There’s a way to build a static (.a
) library as well.
In general, Swift Module can include more than one Swift file. It just so happens that we have only one. To build a module you need to run this command
# Get location of OS X SDK.
export OSX_SDK=$(xcrun --show-sdk-path --sdk macosx)
# Build StdIO module (use `xcrun swiftc` for OS X 10.9).
swiftc -sdk ${OSX_SDK} \
-emit-library \
-emit-module StdIO.swift \
-module-name StdIO
The options are instructing compiler to emit Swift module and shared library. Compiler also needs to know which SDK to build for. As expected, you should have 3 new files created.
StdIO.swiftdoc
StdIO.swiftmodule
libStdIO.dylib
That’s great, the module is ready. Next step is to link it with your main Swift file and run the code. Before you run any command in the shell, you need to add one more line to solve-me-first.swift
file.
# This line is new, import StdIO module
import StdIO
let a: Int = readLn()
let b: Int = readLn()
println(a + b)
Now it’s time to link the two and run the code. This time we can use swift
command.
# Get location of OS X SDK
export OSX_SDK=$(xcrun --show-sdk-path --sdk macosx)
cat tc/solve-me-first-tc0.txt | \
swift \
-sdk ${OSX_SDK} \
-l$(pwd)/libStdIO.dylib \
-I $(pwd) -module-link-name StdIO \
solve-me-first.swift
Let’s talk about each option separately.
-l<library>
- this is a flag that should be followed by the name of the library to link with.- Actually,
-lStdIO
works as well, but that’s becauselibStdIO.dylib
is sitting in the same folder. I choose to be more verbose and use the full path to shared library. My intentions will become clear later when we talk about makefiles.
- Actually,
-I <import-path>
- this flag is used to specify import path. Similar to include path in Objective-C world, this way we tell the compiler where to look for Swift modules to resolveimport
statements in the code.- Once again, instead of specifying current folder as
.
, I’m using full path. That will be explained later.
- Once again, instead of specifying current folder as
-module-link-name <name>
- well, it expects a name of the module to link with.
Actually, this is a bit of a cheat. This second command does not compile the code, but interprets it while linking with existing module. There is a way to use Swift compiler here as well, you can find the reference in the end of this post.
OK, so run this command and you should get a (high) 5
as output. Replace the name of the test case file and the name of Swift file and you can run code for any other assignment. But that is too verbose. It should be automated with…
Makefile
… with Makefile, of course. With small effort we can convert shell script into a makefile script.
# Makefile
# Actual directory of this Makefile, not where it is called form
SELF_DIR := $(dir $(lastword $(MAKEFILE_LIST)))
# Build directory
BUILD_DIR := $(CURDIR)/build
# OSX SDK
OSX_SDK := $(shell xcrun --show-sdk-path --sdk macosx)
# Swift executables and StdIO library name
# TODO: 10.9 use 'xcrun swift(c)', since 10.10 can use just 'swift(c)'
SWIFT = swift
SWIFTC = swiftc
SWIFT_STDIO = StdIO
SWIFT_STDIO_LIB_NAME = lib$(SWIFT_STDIO).dylib
# Test cases configuration
TC_DIR = tc
TC = 0
tc-path = $(TC_DIR)/$(patsubst %.$(2),%-tc$(1).txt,$(3))
# Enable phony targets
.PHONY:
# Swift compile and run
%.swift: .PHONY
@mkdir -p $(BUILD_DIR)
@# build stdio module
@$(SWIFTC) \
-sdk $(OSX_SDK) \
-emit-library \
-o $(BUILD_DIR)/$(SWIFT_STDIO_LIB_NAME) \
-emit-module $(SELF_DIR)/$(SWIFT_STDIO).swift \
-module-name $(SWIFT_STDIO)
@# run linking against stdio module
@cat $(call tc-path,$(TC),swift,$@) \
| $(SWIFT) \
-sdk $(OSX_SDK) \
-l$(BUILD_DIR)/$(SWIFT_STDIO_LIB_NAME) \
-I $(BUILD_DIR) -module-link-name $(SWIFT_STDIO) \
$@
OK, so let’s walk the code.
First we declare 3 variables: SELF_DIR
, BUILD_DIR
and OSX_SDK
.
SELF_DIR
is an absolute path to the location of the makefile itself. We need to know this information, because we also know thatStdIO.swift
is located in the same folder asMakefile
. It is not the same as$(CURDIR)
,$(CURDIR)
is the folder you run makefile from.BUILD_DIR
is the place to put build output. Don’t forget to put it in your.gitignore
and remove it as part ofclean
target.OSX_SDK
is the path to current Mac OS X SDK.
One important note is the use of :=
instead of just =
when assigning values to the variables. If right side of assignment is one of the makefile functions, like shell
to execute shell command, or dir
, then each time you reference a variable, for example $(OSX_SDK)
, the shell command will be executed. If the command is expensive, this will slow down execution of your makefile targets. Using :=
ensures that variable is assigned a value only when initialized, when it’s reference later on it uses that initial value.
Next block declares group of variables used as a reference to Swift executables, StdIO module and library name.
Then there are 2 variables and 2 functions related to test cases configuration. To get more details about tc-path
function, check out previous post. Declaring targets as phony is something that’s explained in that article as well.
Finally, the %.swift
target runs all the compile commands. This code is almost identical to the shell code we had before, with minor differences.
-o <library-name>
option is used to explicitly tell compiler to put library file and accompanying module files into a build directory. That’s just to keep your workspace clean of compiler output.- file name for
-emit-module
is referencing$(SELF_DIR)
variable to findStdIO.swift
in the same folder asMakefile
file.
Give it a try and run it.
# Using `make` directly
make -f ../../Makefile solve-me-first.swift
# Using `hrrun` alias
hrrun solve-me-first.swift
hrrun
is an alias defined in previous article as well.
So it works now and you can keep stdin code separately and reuse it in assignments source. By now you know how to run code in interpreter and how to compile it, wouldn’t it be nice to be able to choose any of the two options?
Interpret or Compile
Since makefiles support if-else flow control statements, you can add a conditional statement and split your makefile code in 2 parts, one for running code using interpreters, another to compile before running. It’s as easy as this
# Makefile
# Compile (YES) vs interpret (NO)
COMPILE = NO
# Enable phony targets
.PHONY:
ifeq ($(COMPILE), NO) # Interpret
# Swift
%.swift: .PHONY
# TODO: Interpret
else # Compile and run
# Swift compile and run
%.swift: .PHONY
# TODO: Compile and run
endif
By controlling value of COMPILE
variable you can choose one of the two ways to run assignments code. Another alias would be handy as well.
HACKER_RANK_HOME="${HOME}/Projects/hackerrank"
alias hrrun="make -f ${HACKER_RANK_HOME}/Makefile"
alias hrrunc="make -f ${HACKER_RANK_HOME}/Makefile COMPILE=YES"
Summary
You can now do the same thing for Haskell, Python, C, C++, Java or any other language that support compilation. Grab the latest version of Makefile for your reference, and happy HackerRanking!