TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

SOLVED: Challenge: can you port this Rust function to Swift?

Forums > Swift

Question

I am trying to port the following Rust function to Swift. This is taken from the book Learning Rust 2nd Ed. from O'Reilly (the code is freely copyable for learning purposes so I don't feel bad about pasting it here).

I just can't get a generic Swift function to work and was hoping to throw this out there for those who fancy a challenge. It's been bugging me that I can't find a solution so it's time to let the experts loose on it!

/// Parse the string `s` as a coordinate pair, like `"400x600"` or `"1.0,0.5"`.
/// 
/// Specifically, `s` should have the form <left><sep><right> where <sep> is
/// the character given by the `separator` argument, and <left> and <right> are
/// both strings that can be parsed by `T::from_str`. `separator` must be an
/// ASCII character.
/// 
/// If `s` has the proper form, return `Some<(x, y)>`. If it doesn't parse
/// correctly, return `None`.
fn parse_pair<T: FromStr>
    ( s         : &str
    , separator : char
    )
-> Option<(T, T)>
{
    match s.find(separator) {
        None => None,
        Some(index) => {
            match (T::from_str(&s[..index]), T::from_str(&s[index + 1..])) {
                (Ok(l), Ok(r)) => Some((l, r)),
                _ => None
            }
        }
    }
}

We would invoke it in a Rust program like this:

let bounds = parse_pair("4000x3000", 'x')

Now the rust compiler is very clever and even though this is the first declaration of "bounds" it can see that it's passed into another function later on as a (usize, usize) tuple so it automatically casts <T> in the above function to <usize> (which is basically a UInt in Swift). If we wanted to be explicit, we'd say

let bounds = parse_pair::<usize>("4000x3000", 'x')

What we have here is a generic function which expects the type to implement the FromStr trait -- basically the target type needs to provide a from_str method which takes a string and returns an instance of itself constructed from that string.

Aside: Rust does not require a return if the final line in a function is an expression and it's idiomatic to omit it. So this function is implemented as a single match expression -- equivalent to a switch expression in Swift. It calls from_str() and checks the Result to see if it's Some(<T>) or None and ultimately constructs a tuple if everything falls out correctly.

Hopefully that all makes sense. Once you get used to it Rust's syntax is quite succinct and direct. Note that &str in the function declaration is actually a type not an address. It indicates that the parameter s is a reference to a UTF-8 string. Elsewhere in the function, s is dereferenced using &s which works as you'd expect.

Right, that's the background. Let's switch over to Swift!

The first thing that we need to do in Swift is see if there's a protocol which is equivalent to FromStr. And... there isn't. The closest we can come to is Decodable which keeps the compiler happy but doesn't quite work. More on that later.

The other thing to note is that the from_str() function returns a Result, not an Option.

Swift Decodable's init() signature doesn't return a Result though: instead, it throws if it fails.

Another thing about the Rust function is that the find() method on the string returns an integer index even though Rust &str strings are UTF-8. Swift would return a String.Index? type which, alas, doesn't do integer arithmetic. So we have to do some gymnastics to find the characters after an index.

Another difference with Swift is that it doesn't allow you to pass the type to a generic function invocation. So you can't do this:

parse_pair<Int>(s: "4000x300", separator: ",")

The solution is to pass the type in to the function as a parameter. This simplifies the job of the compiler (I guess) and it's also a solution that Apple uses in several places.

Anyway, putting it all together, this is the best I could come up with:

func parse_pair<T: Decodable>
    ( _ type    : T.Type
    , s         : String
    , separator : Character
    )
-> Optional<(T, T)>
{
    guard let i = s.firstIndex(of: separator) else {
        return nil
    }

    guard let left = try? T(from: s[..<i] as! Decoder) else {
        return nil
    }
    guard let right = try? T(from: s[s.index(after: i)...] as! Decoder) else {
        return nil
    }

    return (left, right)

}

This compiles. However, if I try to invoke it like this:

let bounds = parse_pair(Int.self, s: "4000x3000", separator: "x")

I get a lovely error message:

Could not cast value of type 'Swift.Substring' (0x7ff84a9a5cc0) to 'Swift.Decoder' (0x7ff84afac788).

At this point I'm going to mull it over and try again tomorrow. It can't possibly be this hard!

What does everyone think? Add an Extension to String to give it a Decoder? Define a new protocol FromStr and extend Int, Double, and Bool (my target types) to implement a from_str() method?

Or am I missing something already built in to Swift?

Apologies for the length of this and for polluting the forums with non-Swift code! And thanks for making it this far.

3      

Update: I found a solution! The appropriate protocol is LosslessStringConvertible. The result becomes pretty easy now:

func parse_pair<T: LosslessStringConvertible>
    ( _ type    : T.Type
    , s         : String
    , separator : Character
    )
-> Optional<(T, T)>
{
    guard let i = s.firstIndex(of: separator) else {
        return nil
    }

    guard let left = T(String(s[..<i])) else {
        return nil
    }
    guard let right = T(String(s[s.index(after: i)...])) else {
        return nil
    }

    return (left, right)

}

🤦

3      

Hacking with Swift is sponsored by Blaze.

SPONSORED Still waiting on your CI build? Speed it up ~3x with Blaze - change one line, pay less, keep your existing GitHub workflows. First 25 HWS readers to use code HACKING at checkout get 50% off the first year. Try it now for free!

Reserve your spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.