Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unbox #31

Closed
Evertt opened this issue May 13, 2016 · 15 comments
Closed

unbox #31

Evertt opened this issue May 13, 2016 · 15 comments
Labels
enhancement New feature or request

Comments

@Evertt
Copy link
Contributor

Evertt commented May 13, 2016

After watching the video and looking at the readme in the repository I'm convinced that Unbox could be a welcome addition to Fluent.

Unbox is a little library that unboxes any object from dictionary data with very simply syntax:

let user = try Unbox(dictionary) as User

So instead of requiring Entity objects to implement an init(serialized: [String: Value]), we would just inherit from the Unboxable protocol and require Entity objects to implement init(unboxer: Unboxer).

The code just looks so much cleaner. It would turn this:

struct User: Entity {
    let id: Int?
    let age: Int
    let name: String
    let gender: Gender // This is an enum
    let birthday: NSDate

    init(serialized: [String:Value]) throws {
        let dateFormatter = NSDateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"

        id = serialized["id"]?.int // id is optional so that's easy
        age    = serialized["age"]?.int ?? 18 // age is not optional, but let's provide a fallback value for ease

        if let name = serialized["name"]?.string,
            let gender = Gender(rawValue: serialized["gender"]?.string ?? ""),
            let birthday = dateFormatter.dateFromString(serialized["birthday"]?.string ?? "") {
            self.name = name
            self.gender = gender
            self.birthday = birthday
        } else {
            // Name, gender and birthday are required,
            // so if they are not present, throw an error
            throw SomeError()
        }
    }
}

Into this:

struct User: Entity {
    let id: Int?
    let age: Int
    let name: String
    let gender: Gender
    let birthday: NSDate

    init(unboxer: Unboxer) {
        let dateFormatter = NSDateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"

        id       = unboxer.unbox("id") // Since id is optional it will not fail when id is not present
        age      = unboxer.unbox("age") ?? 18 // Age is required, but I provided a fallback value so it won't fail
        name     = unboxer.unbox("name") // As you can see it automatically returns the correct type
        gender   = unboxer.unbox("gender") // And it even works for more complex types like enums
        birthday = unboxer.unbox("birthday", formatter: dateFormatter) // and dates and even structs and classes
    }
}

I think it is 100x nicer to read. So, what do you guys think?

The same guy also made a Wrap library which basically does the inverse of Unbox. If we would implement Wrap too then that would satisfy the other issue I opened #30

@Evertt Evertt added the enhancement New feature or request label May 13, 2016
@Evertt Evertt changed the title Implement (some variant) of Unbox and maybe also Wrap Implement (some variant of) Unbox and maybe also Wrap May 13, 2016
@Evertt
Copy link
Contributor Author

Evertt commented May 13, 2016

I would also like to combine this feature in some way with Vapor's validators

@tanner0101
Copy link
Member

This is interesting. I really like the cleaner syntax.

Let's revisit this when Fluent is in a more functional state. The underpinnings of how everything will work are still not really decided.

@Evertt
Copy link
Contributor Author

Evertt commented May 13, 2016

@tannernelson I was already really excited to start working on this, but okay, I'll hold myself. :-p

@tanner0101
Copy link
Member

Fluent is going to be top priority this next week, so expect to see some good progress there soon. :)

@tanner0101 tanner0101 changed the title Implement (some variant of) Unbox and maybe also Wrap unbox May 18, 2016
@Prince2k3
Copy link
Member

I like this a lot actually.

@NathanFlurry
Copy link
Contributor

I think it'd be great to combine this with some rendition of Argo to make it really, really easy to make serialized objects.

@Evertt
Copy link
Contributor Author

Evertt commented May 25, 2016

@NathanFlurry, could you show a code snippet of how that would make things even better than with only using Unbox?

@NathanFlurry
Copy link
Contributor

Let's take the example you provided. First of all, I'd probably rewrite it to be like this using Unbox, so I can reuse any extra code I write in my default initializer:

struct User: Entity {
    let id: Int?
    let age: Int
    let name: String
    let gender: Gender
    let birthday: NSDate

    init(id: Int?, age: Int?, name: String, gender: Gender, birthday: NSDate) {
        self.id = id
        self.age = age ?? 18
        self.name = name
        self.gender = gender
        self.birthday = birthday
    }

    init(unboxer: Unboxer) {
        let dateFormatter = NSDateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"

        self.init(
            id: unboxer.unbox("id"),
            age: unboxer.unbox("age"),
            name: unboxer.unbox("age") ?? 18,
            gender: unboxer.unbox("gender"),
            birthday: unboxer.unbox("birthday", formatter: dateFormatter)
        )
    }
}

With something like Argo (I'll just use their current DSL for this example, I'm not saying this is the best way to implement the API):

struct User: Entity {
    let id: Int?
    let age: Int
    let name: String
    let gender: Gender
    let birthday: NSDate

    init(id: Int?, age: Int?, name: String, gender: Gender, birthday: NSDate) {
        self.id = id
        self.age = age ?? 18
        self.name = name
        self.gender = gender
        self.birthday = birthday
    }

    static func decode(j: JSON) -> Decoded<User> {
        return curry(User.init)
            <^> j <| "id"
            <*> j <| "age"
            <*> j <| "name"
            <*> j <| "gender"
            <*> j <| "birthday"
    }
}

While there's not too much of a difference in the end, it's nice to not have to write things like id, age, and name two times on each line. Ideally, if something similar to Argo were to be implemented, it would use Unbox for retrieving and casting the values and use Curry to save the amount of code required to be written.

@loganwright
Copy link
Member

loganwright commented May 27, 2016

I'm super biased because I have my own mapper, but if we're going to introduce a 3rd party mapper, I'd strongly prefer anything but Argo. It's concepts are excessively complex and difficult to work with.

A large part of Genome above is focusing on super readable and clear error messages. It's also more customizable w/ transforms and utilizes the throwing system in a way that is easier in the long run.

I think I'd prefer not to include 3rd party mappers in core if possible. If people really want one, I'd like to throw Genome into the evaluation mix.

For ref:

struct User: MappableObject {
    let id: Int?
    let age: Int
    let name: String
    let gender: Gender
    let birthday: NSDate

    init(map: Map) throws {
        let dateFormatter = NSDateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"

        id = try map.extract("id") // Since id is optional it will not fail when id is not present
        age = try? map.extract("age") ?? 18 // Age is required, but I provided a fallback value so it won't fail
        name = try map.extract("name") // As you can see it automatically returns the correct type
        gender   = try map.extract("gender") // And it even works for more complex types like enums
        birthday = try map.extract("birthday", transform: dateFormatter.dateFromString) 
    }
}

@Evertt
Copy link
Contributor Author

Evertt commented May 27, 2016

I prefer Unbox over Genome for two reasons:

  1. The syntax of unbox is a bit less cluttered with throws, try and try? like Genome's syntax is.
  2. Thanks to my pull-request, Unbox now throws an array of errors when it fails, showing the user everything that's wrong with the input instead of just the first thing that was wrong.

Oh and Unbox offers transform just like Genome. I'm open to the idea to just fork Unbox into Qutheory and maintain it ourself / myself. Then I could even customize it so it would work super nicely with the Valid type you made.

@loganwright
Copy link
Member

loganwright commented May 27, 2016

I agree that it's less cluttered, but I'm very hesitant to use a mapper that's going to crash at runtime which unbox seems like it would have to do since it's not implemented as throwing.

I think this is why mappers should maybe stay out of Fluent where possible. People have very different opinions on them (most of which are valid), probably why there's so many.

The other issue I'm concerned about w/ unbox is that it doesn't seem to have a way to go Model => Data.

if we were to include a default mapper, being able to serialize both ways with one syntax is ideal.

@Evertt
Copy link
Contributor Author

Evertt commented May 27, 2016

but I'm very hesitant to use any mapper that's going to crash at runtime which unbox seems like it would have to do since it's not implemented as throwing.

Ah but that's where you don't fully understand Unbox. You see, Unbox does throw exceptions. When you write let user = try Unbox(jsonDictionary) as User then unbox will try to unbox user. However, it will do it in such a way that the initializer of user doesn't need to throw. For example:

struct User: Entity {
    let id: Int?
    let age: Int
    let name: String

    init(unboxer: Unboxer) {
        id   = unboxer.unbox("id")
        age  = unboxer.unbox("age")
        name = unboxer.unbox("name")
    }
}

Let's say that age and name were both not present in the json dictionary so it should fail on those keys. What Unbox does is that it notices age and name are missing, it will save that knowledge inside its own state and to prevent the initializer from failing it will just return default values to age and name. But since it remembered that those keys were actually not there, Unbox will throw an error in the line let user = try Unbox(jsonDictionary) as User. And in that error it will say something like missing values for keys "age" and "name". Do you understand?

Do you see that this has a huge advantage? Namely that unbox will gather everything that went wrong, not just the first thing. In your Genome, it will throw at the first thing that went wrong. That means if an end-user made 5 mistakes in their JSON then they need to retry 5 times. While with Unbox they will see everything they did wrong the first time.

@Evertt can you clarify one other thing for me. Unbox doesn't seem to have a way to go Model => Data.

Indeed, Unbox doesn't do that. However, the same guy made another repo called Wrap which does exactly that. So he just split it up into two packages. Again, I am very willing to just fork Unbox and make it ours and totally customize it to our needs. I just really really really like the syntax of Unbox, that's why I'm so persistent.

In Swift 4, when generics are hopefully finally completed and so subscripts will be generic, it can even become as beautiful as this:

struct User: Entity {
    let id: Int?
    let age: Int
    let name: String

    init(box: Box) {
        id   = box["id"]
        age  = box["age"]
        name = box["name"]
    }
}

@loganwright
Copy link
Member

Can't wait for full generic support either, that's definitely going to make all the mappers a lot more awesome. Especially when generic extensions can conform to protocols. That's going to allow a lot fewer overloads to support different generic scenarios! To your example, Genome would be:

struct User: MappableObject {
    let id: Int?
    let age: Int
    let name: String

    init(map: Map) throws {
        id   = try box["id"]
        age  = try box["age"]
        name = try box["name"]
    }
}

I understand your persistence, I really love the syntax of Genome, and a lot of people really love the syntax for Argo 😄 I disagree with a lot of the design decisions of Unbox, and don't see it being customized in a way that makes it worthwhile to fork. (I'm sure he disagrees with some of mine as well which is why he built it 😛 )

For now, I'd say people should build support for the mappers they prefer and our organization should stay out of it. I don't see a way to make everyone happy here.

@Evertt
Copy link
Contributor Author

Evertt commented May 28, 2016

@loganwright I wonder if you read this part of what I wrote:

Do you see that this has a huge advantage? Namely that unbox will gather everything that went wrong, not just the first thing. In your Genome, it will throw at the first thing that went wrong. That means if an end-user made 5 mistakes in their JSON then they need to retry 5 times. While with Unbox they will see everything they did wrong the first time.

Because I'm very interested to hear what you think about it. Do you see it as an advantage? Or do you see it as bad design?

@tanner0101
Copy link
Member

We have this now with extract

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

5 participants