promises-aplus / constructor-spec

Discussion and drafts of a possible spec for creating and resolving promises
10 stars 4 forks source link

point function #24

Closed Raynos closed 11 years ago

Raynos commented 11 years ago

@pufuwozu brought up a good point with his article

http://brianmckenna.org/blog/category_theory_promisesaplus

The notion of having a point function that takes a value and returns a promise would allow for writing powerful higher order functions.

Basically prior art, a-la category theory shows that having a function that looks like point is a good thing, we should consider this.

puffnfresh commented 11 years ago

Specifically, the point function needs to take ANY value and create a fulfilled promise containing that value.

It's a very, very simple function but when combined with then, promises will form a monad. Being a monad allows us to write functions which work the same for many structures. It allows DRY code.

@juandopazo said there was possibly a proposal for a from method. Sounds good to me.

Promise.prototype.constructor = Promise;
Promise.from = function(value) {
    // Implementation of fulfilled promise that contains value
    // ...
};
ForbesLindesay commented 11 years ago

I'm very keen for something broadly of this nature. I really want a way to take a promise, and create a completely new promise of the same library (i.e. with the same extension methods and helpers). Almost any time I'd use this though, I'd actually wan the behavior I've suggested elsewhere in other issues which I've called "assimilate" thus far, which essentially just looks like:

Promise.assimilate = function (valueOrThenable) {
  return new Promise(function (resolve) { resolve(valueOrThenable); });
};

This would be considerably more convenient for the use cases I have in mind, but by the sounds of things wouldn't help your use case at all. It would always be fulfilled with a value that was not a thenable/promise (or be rejected if the thenable/promise was rejected).

Could you try and explain why we need a way to create nested promises? Preferably in terms that don't require much knowledge of category theory, as I (and probably many others reading these posts) don't have vastly more knowledge of it than what you mentioned in your blog post.

puffnfresh commented 11 years ago

@ForbesLindesay it's surprisingly very, very simple. The first reason is a concept called parametricity. Basically, given a function like the following:

point :: a -> Promise a

We can't tell on the inside of the implementing function what the structure of a actually is. Of course in JavaScript we can rely on duck-checking, tag-checking or something similar but that breaks the parametricity law. Breaking the law might be useful in some cases but now other laws will break!

So immediately, the next law I see breaking is the "left identity" monad law. It says these two things should always be equal:

What happens when a is a promise? If from "flattens" it then f will get the fulfilled content of a! Definitely not the same as just calling f(a).

Why are laws useful? Couple of reasons:

Quoting "What happens to you if you break the monad laws?":

In short, "breaking the monad laws" should generally be read as "writing buggy code".

Raynos commented 11 years ago

Breaking the monad laws is like the difference between a promise and a jQuery deferred. i.e. subtle bugs and having to write special cases :(

ForbesLindesay commented 11 years ago

@pufuwozu thanks, that was a nice explanation.

@Raynos doing something that bad would indeed be terrible.

I still think what's needed now is an example of something practical (that could be seen in a typical application) that would benefit from the monad behavior over non-monadic behavior. I think the difficulty you'll have persuading people to accept this is that most people still view promises as representations of future values (or errors) rather than as a special case of monads.

Coming from that it's difficult to see why those cases matter. I don't see anything else in JavaScript (e.g. an array) behaving like a Monad in a way that lets me write a function that's clear and simple to reads and does something useful to both a promise (with the point extension) and an array (presumably with a similar point extension).

techtangents commented 11 years ago

I think the difficulty you'll have persuading people to accept this is that most people still view promises as representations of future values (or errors) rather than as a special case of monads.

Well, they're both. Monad is an interface that a promise implements. As a result of implementing this interface, a large set of functions become available. It's a tremendous amount of free code for the cost of implementing 2 methods.

You'll find that a large set of types are monads. Combine this with the large amount of operations that apply to all Monads, and you can see that Monad is an incredibly powerful abstraction.

Lots of things are Monads and each one of them gets a stack of functions for free. What's there not to love?

It also comes at no detriment to your code. I have yet to see a single feature of the promise specs that would break as a result of implementing Monad. It's win-win.

I don't see anything else in JavaScript (e.g. an array) behaving like a Monad in a way that lets me write a function that's clear and simple to reads and does something useful to both a promise (with the point extension) and an array (presumably with a similar point extension).

Note that Monad is an abstraction, so the operations that arise from Monad are probably more abstract than you're used to. As they apply to different data types with different flatMap/point implementations, they will mean different things to different types - but the differences are encapsulated in the flatMap/point implementation - the abstract function just deals with those functions. In essence, Monad is a pattern - you demonstrate how your type matches that pattern, then the Monad operations deal with the pattern itself.

I'll give you an example: flatten.

If I have a Promise of a Promise of an x, I can flatten that to get a Promise of an x. If I have an Array of Arrays of x, I can flatten that to get an Array of x. If I have an Option of Option of x, I can flatten that to get an Option of x. ... repeat for all Monads... and there are a lot of them

All with one function. I don't have to write flatten for each type, I just write it once for Monad and it just works for all Monads.

Now, Monad is just one of a set of type classes / interfaces that are generally useful. Other common ones are Functor and Applicative. All Monads are Applicatives and all Applicatives are Functors. Same as Monad, if you implement these other type classes, you gain a library of functions for free.

More things are Functors than Monads, but Monad gives you more operations for free. That's why it's one we really like. Note the function "liftA2" that Brian mentioned above - the 'A' stands for Applicative. So, the type doesn't need to be a Monad to use this function - it only has to be an Applicative, which is slightly less restrictive and has some nice properties that Monad doesn't have.

And this is really just dipping your toes in the water. Open your minds to these new abstractions and a whole new world of awesome opens up right before your eyes.

techtangents commented 11 years ago

BTW - here's a future library I developed ages ago: https://github.com/techtangents/jsasync

It uses "Futures" and "Asyncs" - I'm not sure if my terminology is in line with everyone else, though. Anyway, its future is a value that might be available in the future. Its Asyncs are a wrapper around functions that return a future.

In this library, I've implemented Functor, Applicative and Monad for Future, and Category and Arrow for Async. Category and Arrow are two other very useful abstractions that abstract things like functions.

If you've come across function composition, you've encountered Categories. Composition of 2 functions generalises to composition of two Categories.

puffnfresh commented 11 years ago

Concrete example:

// Constructor
function Person(name, age, postcode) {
    this.name = name;
    this.age = age;
    this.postcode = postcode;
}

var me = lift3(Person, name, age, postcode);

The lift3 will give us a Person only when all three fields "have values".

In our application code, let's pretend that name, age and postcode are promises:

name = httpGet('name');
age = httpGet('age');
postcode = httpGet('postcode');

But we don't want to make HTTP requests in our unit tests! Let's make them "optional values" to see if the logic still works:

name = someValue('Brian');
age = someValue(23);

postcode = noValue;
run();
assert(me == noValue);

postcode = someValue(4017);
run();
assert(me.get().postcode = 4017);

Both promises and optional values are monads with similar semantics. The me value only relies on the monad class so we can make it asynchronous or synchronous just by changing the monad (i.e. the wrapper around name, age and postcode)!

ForbesLindesay commented 11 years ago

OK, thanks, this has been really useful and informative. What I'd like to propose we do to move things forwards is:

  1. Make sure we don't prohibit having a method to create a promise for a promise in any of the specs (I still don't fully understand the implications of doing so, but I'll take your word for the fact that it's important).
  2. Create a separate spec for how "Monadic Promises" should work. This would include the point, flatMap and onRejected operations, with exactly the semantics desirable for them to be monads.

This spec needs to include a duck typing test for "Monadic Support" (I'm not sure if "monadic" is the right word for "like a monad"). It will be optional, in the sense that not all promise libraries will support it, but if it proves useful, many will support it. You can then write libraries that require their promises implement the "Monadic Promises" spec. They can just either wrap promises that don't implement "Monadic Support" or throw a TypeError.

This will keep the core APIs for consuming and creating promises simple for people who have approached the problem from a software development background (e.g. callbacks) rather than a category theory background, because they can continue to use the simple to understand .then method and the constructor proposed here. But it will allow libraries to support more advanced functionality.

ForbesLindesay commented 11 years ago

If @paulmillr, @Raynos, @pufuwozu, @techtangents and the obvious key individuals in the promises-aplus organisation are happy with that as a way to proceed, someone can create another repository for this monad spec.

Additionally, I wonder whether the Category Theory/Monads world has anything interesting to say about cancellation or progress, as both specs are still very much a work in progress and they're thorny issues which are not yet well understood via being implemented & used.

techtangents commented 11 years ago

There's some discussion on this thread: http://stackoverflow.com/questions/8556746/monad-transformer-for-progress-tracking

paulmillr commented 11 years ago

+1 for from method. ECMAScript 6 specifies Array.from and maybe Set.from.

bergus commented 11 years ago

-1 for using from as in ES6 Array.from. It is more like our assimilate procedure, and I would expect Promise.from to take any thenable and transform it into a valid promise.

Instead, I'd opt for Promise.of as the point/return function whose point is to wrap a value into a Promise for it. This resembles Array.of, and I think it is also more descriptive than "from".

puffnfresh commented 11 years ago

@paulmillr wow, thanks for that. I took a look at the spec - from is not what we want, we want of!

http://people.mozilla.org/~jorendorff/es6-draft.html#sec-15.4.3.4

> Array.of(1)
[ 1 ]

I propose we name this method Promise.of - it will work very similar to Array.of.

techtangents commented 11 years ago

+1 As an aside: Google Guava also uses 'of' in constructing their data structures. e.g. ImmutableList.of(3).

juandopazo commented 11 years ago

So immediately, the next law I see breaking is the "left identity" monad law. It says these two things should always be equal:

  • Promise.from(a).then(f)
  • f(a)

We've run into this issue when discussing the behavior of resolver.fulfill. The problem is with the overloaded then. Take the identity function:

function identity(x) {
  return x;
}

One would expect that promise.then(identity).then(identity) would return a promise for the same value as the first promise. However, if point lets you create a promise of a promise, then

Promise.point(somePromise).then(identity).then(function (value) {
  assert(value === somePromise); // false
});
ForbesLindesay commented 11 years ago

One of many reasons that a "promise for a promise" is undesirable. When @domenic said you need to start by writing code, I think he was correct. Begin by writing something that does Promises/A+ but with your extensions. https://github.com/nathan7/pledge/blob/master/pledge.js is about the most minimal implementation I've seen, so would be a good starting point. build your promise monad, make it still pass the promises/A+ test wuite (shouldn't be too difficult). Build a cool (and open source) real-world application that uses it, refine it as needed, then lets talk about incorporating the good bits into currently active promise libraries.

I'm voting to close this issue now as I don't think point or of is going to make it into v1 of the resolvers-spec.

techtangents commented 11 years ago

I think this thread just took a turn for the worse.

Can you please explain what's going on here?

One would expect that promise.then(identity).then(identity) would return a promise for the same value as the first promise. However, if point lets you create a promise of a promise, then

Promise.point(somePromise).then(identity).then(function (value) { assert(value === somePromise); // false });

techtangents commented 11 years ago

We've run into this issue when discussing the behavior of resolver.fulfill. The problem is with the overloaded 'then'

Perhaps this is the problem. Having functions that do different things depending on the type is a bad idea. It impacts your ability to reason about what the function does, and breaks the parametricity law.

Looking at http://brianmckenna.org/blog/category_theory_promisesaplus, it looks like you're trying to use 'then' to be map and flatmap - this is a bad idea. They're completely different concepts. For a promise, flatMap chains the result of an asynchronous computation into another asynchronouse computation, returning a Promise of the combined result. Map, on the other hand, just runs a (synchronous) function over the output. Very different use cases.

If you're writing a spec to be used by multiple implementations, you can't afford to make this sort of mistake. It just has "bug" written all over it.

Let me be clear: parametricity lets you reason about a type-parameterised function independently of the type being passed in. i.e. you can reason about what effect map or flatMap independently of what type of value is being passed in. This is important everywhere, but has particular importance for a dynamically-typed language.

techtangents commented 11 years ago

Let me use an array analogy.

map for arrays maps a function over each element in an array. e.g. map([1, 2, 3], function(x) { return x + 1; }); returns [2, 3, 4];

flatMap for arrays maps a function that returns another array over the array, then collapses the result. e.g. flatMap([1, 2, 3], function(x) { return [x, String(x)]; }; returns [1, "1", 2, "2", 3, "3"];

Now, what if I were to write this: map([1, 2, 3], function(x) { return [x, String(x)]; }; I would expect this to return: [[1, "1"], [2, "2"], [3, "3"]]

Note that map and flatMap of the same function over the same array return different values.

However, if I only had a "then" function that chose map and flatMap, one of those behaviors would be impossible to write.

juandopazo commented 11 years ago

Yes, that's the issue with promises for promises. And IMHO it's unfortunately too late to fix it. There is a lot of code out there that depends on this being "broken".

techtangents commented 11 years ago

Well, isn't it just the "then" function that has this mixed map/flatMap behavior? Sorry if this has already been asked, but could you just add map and flatMap? "then" behaves as-is, and map and flatMap behave like the Functor and Monad interfaces require.

Would this be a workable solution? Old code keeps working and you get to implement Monad.

puffnfresh commented 11 years ago

I think I've been pretty good with giving examples of where things will break. Can someone do the same with of?

I don't know the reason why it can't be properly implemented. Thanks.

ForbesLindesay commented 11 years ago

It can't be implemented if you require promises for promises using the existing then behavior. That isn't going to change because promises for promises don't actually make sense as a concept. It makes sense for some monads, but not for promises. As such, it's not going to change.

I'd suggest you create an orthogonal spec that specifies the behavior of a flatMap method, but don't overload then if you want anyone to actually use it.

puffnfresh commented 11 years ago

Promises of promises do make sense as a concept. I've used them in other implementations.

To be a monad, it has to allow nesting (like I showed above), otherwise it is not a monad.

What I think you're saying is that Promises/A+ can not define a monad because it can not define an of function. I am trying to ask "why?"

I have pointed at laws and given example code whenever I say something won't work. Can someone please do something similar?

If it is a problem with Promises/A+ then that is sadly a missed opportunity :(

People are already using Fantasy Land but thanks for the feedback.

juandopazo commented 11 years ago

I have pointed at laws and given example code whenever I say something won't work. Can someone please do something similar?

I did. See above: https://github.com/promises-aplus/resolvers-spec/issues/24#issuecomment-16243660

puffnfresh commented 11 years ago

@juandopazo ah thanks, forgot to reply to that.

Functor laws as specified in Fantasy Land:

It's really great to see that you want to preserve the first Functor law! In a language with types, it's proven to always be true so we don't even have to worry about it (due to parametricity). How cool is that!?

Anyway, if we were able to of a promise to a promise and get a nested promise then it would trigger then's overloaded functionality and it wouldn't behave like map :disappointed:

But then we satisfy the monad laws and can derive lots of useful methods. For example, a map that satisfies the functor laws!

function map(p, f) {
    return p.then(function(a) {
        return p.constructor.of(f(a));
    });
}

So you see that you're saying that having no of means that we can have a map that works for any function that doesn't return a promise. I'm saying that having an of means we can implement a map that works for any function that returns anything!

More equivalence laws the better. Laws allow us to derive useful functionality and easily reason about what code will do what. You've said that we want the functor laws. Great. Why don't we want to add one function to satisfy the monad laws as well?

juandopazo commented 11 years ago

That sounds nice, but it's sadly breaking expectations one way or another. With the overloaded then we either break the left identity law or we break the first functor law. I'd say we're in a pickle.

puffnfresh commented 11 years ago

@juandopazo so if of is doesn't work for promise values, then there will be broken laws. If of does work for promise values, it's possible to work around and create methods that don't have broken laws.

I really, really want a non-broken of so I can work around it - that's a much better pickle.

juandopazo commented 11 years ago

@pufuwozu how would you work around the broken functor law?

ForbesLindesay commented 11 years ago

The reason a promise for a promise doesn't work is because a promise isn't just a mathematical concept. It represents something concrete:

A promise represents a value that may not be available yet.

It isn't really a thing in and of itself, it's just a representation of a thing we don't have yet. Therefore if is fulfilled, we should have a value. It simply doesn't make sense for a value that may not be available yet to become available and still be a value that may not be available yet.

The same logic applies fairly well to option types, but not to arrays. I personally don't see that much practical use for sharing code between promises and arrays. The only one that I see making obvious sense is flattening, but our promises always remain as flattened as possible.

I'd love to see an example of a monad, which makes sense being nested, that can usefully share code that also works for promise monads.

puffnfresh commented 11 years ago

@juandopazo the map function that I defined above satisfies both functor laws when given a working of:

function map(p, f) {
    return p.then(function(a) {
        return p.constructor.of(f(a));
    });
}
puffnfresh commented 11 years ago

@ForbesLindesay promises can be a mathematical concept. It's called a monad.

How about the identity monad as an example?

function Id(a) {
    this.value = a;
}
Id.of = function(a) {
    return new Id(a);
};
Id.prototype.then = function(f) {
    return f(this.value);
};
puffnfresh commented 11 years ago

Taken from work: we represent everything as a future value but switch to the identity monad in tests (so that they're deterministic). Extremely useful.

ForbesLindesay commented 11 years ago

No, I get that the identity momand exists, it's not an example, unless you have some code that:

  1. works for both the identity monad and a promise monad
  2. breaks if the promise monad doesn't support nesting
  3. can't be done in an alternative way that's much clearer

Yep, I often do a similar thing in tests. I represent all asynchronous results as promises, then switch to a promise that's already resolved in tests. In general that's done by writing new Promise(function (resolve) { resolve(value); }), but many libraries provide a helper (e.g. Q provides Q(value)).

I have no nested promises, so I have no need to have nested monads in my tests, I just use promises that are already resolved.

ForbesLindesay commented 11 years ago

Nested Identity monads don't really make a whole lot of sense either. The only example I've seen so far that does is the Array, and although I can see that some code could be shared between promises and arrays, they're really pretty different beasts, and probably better off that way.

puffnfresh commented 11 years ago

@ForbesLindesay we do have code that works in the identity monad and a promise monad. It would break if the promise monad didn't support nesting. It can't be done an alternative way - we rely on the monad abstraction.

I'm not lying about this maths stuff being a useful abstraction. We use it and things break when they're not done properly.

The identity monad has nothing to do with arrays. What's that about?

ForbesLindesay commented 11 years ago

Show me an example of such code. I've given you 3 points as requirements, I think they're fair, and I think I could meet equivalent requirements for two promise libraries and any current feature in the promises spec.

In your example, you might also want to try and answer the following 3 questions

I look forward to reading your example.

(as for the point about arrays, you've mentioned before that arrays are monads, they can have map/flatMap methods, and it makes sense to nest them, but it doesn't make sense to nest any other monad I've come across)

juandopazo commented 11 years ago

@pufuwozu right, map keeps returning a promise for a promise, so map(map(promise, identity), identity) still works. However, the problem I see is discoverability of the issue by unsuspecting developers. If we add of allowing it to wrap promises, they may run into the broken functor law issue when using then and it's pretty hard to figure out. Keep in mind that the identity function is the default behavior for then when the first parameter is undefined.

However, if we don't introduce an of that lets you wrap promises in promises, you could still work arround it by writing your own of function, as long as there is a way to create a promise for a promise, even if it's not convenient, am I right?

puffnfresh commented 11 years ago

@ForbesLindesay my answer is to 1, 2 and 3 is because that's a requirement to satisfy the left identity monad law. If the law is not satisfied, there will be bugs.

Now, you ask, what type of bugs? Now, that's hard. When algebraic laws are broken, subtle things happen. Classic example of associativity of addition of numbers (semigroup):

// Should print the same thing twice. Rule of associativity of addition.
function add(a, b, c) {
    console.log((a + b) + c);
    console.log(a + (b + c));
}

add(1234.567, 45.67834, 0.0004);
// 1280.2457399999998
// 1280.24574

That's subtle detail that you don't usually think about when dealing with floating point numbers. With strings it always works:

add("one", "two", "three");
// onetwothree
// onetwothree

You have to be careful whenever you use addition! Some things look like they support it, but they actually support a broken version.

Because these laws are in some sense "part of" the type class, it should be reasonable for other code to expect they will hold, and act accordingly. Misbehaving instances may thus violate assumptions made by client code's logic, resulting in bugs, the blame for which is properly placed at the instance, not the code using it.

The monad laws involve a bit more code to break - but when they'll break, that'll make it even more subtle:

In short, "breaking the monad laws" should generally be read as "writing buggy code".

I'm going to attempt an example - but this doesn't show up with just one line of code unless you point at join. It's a combination of code that results in bugs.

puffnfresh commented 11 years ago

@juandopazo if you specify an of that does something special when Promises are involved, it's not possible to write a correct of from the specification.

ForbesLindesay commented 11 years ago

So far I've seen "Appeal to Authority" and the analogy of associativity of addition. I knew all that about addition, but we've come to terms with the fact that Q(v).then(fn) is not the same as Q(fn(v)). Nor should it be. Other than that one, I haven't seen any other examples of seemingly obvious rules being broken.

Still eagerly awaiting actual code replacing appeal to authority.

puffnfresh commented 11 years ago

@ForbesLindesay "Nor should it be" - yes it should, according to the law! The authority is an algebraic law. Come on.

I'll try to find an example but do you really think that algebraic laws are not important?

ghost commented 11 years ago

@ForbesLindesay What an odd complaint! We accept the authority of mathematical logic in order to be able to explain existing applications of the logic and predict the results of future applications of the logic. This is part and parcel of what it means to program—by the Curry-Howard Isomorphism, if you want yet another "appeal to authority"—and it's a very poor reflection on the JavaScript community to place actual appeals to authority, i.e. existing code, culture, tradition, aesthetic judgment, etc. etc. etc. literally ad nauseam, before actual reasoning.

juandopazo commented 11 years ago

@pufuwozu yeah I wasn't very clear. What I meant is that I wouldn't want to provide a short form that could lead to bugs with then, because most developers will jump to use the short form. But I am thinking maybe we should add a more obscure way to do it so you could still write your own point function.

For example, the DOMFuture way of creating promises is:

var promise = new Future(function (resolver) {
})

Where:

In that case you could monkey patch Future and add the methods you need:

Future.point = function (value) {
  return new Future(function (resolver) {
    resolver.accept(value);
  });
};
Future.prototype.map = function (fn) {
  var promise = this;
  return promise.then(function (value) {
    return promise.constructor.point(fn(value));
  });
};

...and you'd have a nice monad-compatible implementation.

puffnfresh commented 11 years ago

@juandopazo I'm happy for a solution. I'm not happy that making things buggy for the sake of making buggy things less buggy in a certain situation.

I think making a non-broken of is the right thing to do. The behaviour of then is a problem. That should stay a problem with then and not lead to introducing problems across the specification.

ForbesLindesay commented 11 years ago

@pufuwozu @psnively these are not appeals to algebra. You haven't presented a mathematical proof. You are applying the semantics you believe should exist. If promises were monads, then the semantics you provide would be correct, but they're not. As such, the current semantics are equally valid and useful. The current semantics allow me to take an object which is promise-like but perhaps not a promises/A+ promise and convert it to a real promise. Yours allow you to satisfy your desired aesthetics, which you have chosen to label as algebra.

If you can present a mathematical proof that Q(v).then(fn) should be equivalent to Q(fn(v)) without making arbitrary assumptions as to the semantics of Q and then I will of course back down. As it stands, the current semantics are easy to reason about and useful in real world programs.

juandopazo commented 11 years ago

@pufuwozu I think you're thinking buggy in the context of the code you want to write. But most people using promises will just be interacting with then, not building other abstractions on top of it. In fact, most peple already have problems understanding promises and how to use them. That's why I think we shouldn't standardize a short form that could lead them to bugs with then, but we should leave the door open for you to be able to write a useful map.

copumpkin commented 11 years ago

I think the point is that, regardless of whether @ForbesLindesay says that nested promises "don't make sense", the fact remains that you could easily implement them to work the way @pufuwozu says.

Monad is an interface, Promise is a computational concept that exists widely outside of javascript or this particular hypothetical implementation. Promise can easily be made an instance of Monad, and has been in many other implementations. Whether you want that or not is another question, but none of the reasons ("appeal to lack of imagination" it appears) stated so far for not doing it seem particularly convincing. The fact that the lack of imagination extends so far as to saying that nested option or identity types don't make sense, you'd think you'd just look up examples of why nesting is a desirable property (and is fundamental to monad-ness) rather than just talking about how it doesn't make sense. People have been talking about this for literally decades.

ForbesLindesay commented 11 years ago

I continue to wait for a real example of code. Contrary to what you have been saying, these are eminently not provable. You can make decisions about the semantics of various methods and then prove things about what behavior certain programs have. You could also make assumptions about the behavior of certain programs and work backwards to prove what the semantics of various methods must be. You can't prove what the semantics of both should be, because it's not a mathematical absolute, it's a trade-off of pros and cons.