thoughtbot/Argo

Mapping decoded arrays with array functions (or T with T's functions)

jshier opened this issue · 9 comments

General Question:
I'm decoding an array of a given type, but I'd like to drop the first element of the array when returned the decoded types. Naturally, dropFirst can be used here. Is there any way to map such a function into the decoding?

In Particular:
I'm parsing my CocoaPods acknowledgements files, and it's first array item is something I want to ignore. The decoding is otherwise trivial:

struct Acknowledgements {
    
    let acknowledgements: [Acknowledgement]
    
}

extension Acknowledgements: Decodable {
    
    static func decode(_ json: JSON) -> Decoded<Acknowledgements> {
        return self.init <^> ((json <|| "PreferenceSpecifiers") <^> Array.dropFirst)
    }
    
}

struct Acknowledgement {
    
    let title: String
    let licenseText: String
    
}

extension Acknowledgement: Decodable {
    
    static func decode(_ json: JSON) -> Decoded<Acknowledgement> {
        let cinit = curry(self.init)
        
        return cinit
            <^> json <| "Title"
            <*> json <| "FooterText"
    }
    
}

However, I see no elegant way to return the array of items with the first element dropped. Either I'll have to do the parsing more manually or I'll have to create a global function which takes an array and returns the array with the first element dropped.

So the type of dropFirst is [A] -> [A] and the type you have after decoding the list is Decoded<[Acknowledgement]>. If you ignore the Array it comes down to A -> A and Decoded<A> ... now this looks perfect for map ie <^>.

I can see you got that far. I think you just mixed up the types. <^> takes the function as it's first argument (left side). (Array.dropFirst <^> (json <|| "PreferenceSpecifiers")) should do the trick.

Actually, the signature is dropFirst() -> ArraySlice<Element> which is a pain. So I need to map the Array initializer in there somewhere. return self.init <^> ((Array.dropFirst <^> (json <|| "PreferenceSpecifiers")) >>- Array.init(_:)) doesn't work. Nor does (Array.init(_:) <^> (Array.dropFirst <^> (json <|| "PreferenceSpecifiers"))). After dropFirst, I think I have Decoded<ArraySlice<Acknowledgement>>, so I would've thought <^> would be appropriate again.

At that point I would probably just make an extension on ArraySlice or Array that takes care of that weirdness for you.

You're right that <^> is appropriate again but I think it's going to look pretty gross with it all in there together. Not sure why <^> didn't work for you ... in those situations I always break the statement up into single expression with a typed variable so I can make sure the type at each step is what I think it is.

I'm getting the error: Cannot convert value of type '(_) -> Array<_>' to expected argument type '(_) -> _', probably because Array.init(_:) is generic for a Sequence.

Breaking it out step by step, it looks like dropFirst in this context is returning a Decoded<(Int) -> ArraySlice<Acknowledgement>>, which I'm not sure how to handle at all.

I just broke it out into a global function so I could flatMap it.

extension Acknowledgements: Decodable {
    
    static func decode(_ json: JSON) -> Decoded<Acknowledgements> {
        return self.init <^> (json <|| "PreferenceSpecifiers" >>- dropFirst)
    }
    
}

fileprivate func dropFirst<T>(_ array: [T]) -> Decoded<[T]> {
    return pure(Array(array.dropFirst()))
}

Only now that code is causing a compiler crash. 💥

Making the dropFirst global function I created non-generic fixed the crash. 🤷‍♂️

haha welp glad you got it working! I think there is just some weirdness with ArraySlice and that was causing you some issues.