-
Notifications
You must be signed in to change notification settings - Fork 376
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
add Filterable algebra #274
Conversation
README.md
Outdated
var F = this.constructor; | ||
return this.reduce((f, x) => pred(x) ? f.concat(F.of(x)) : f, F.empty()); | ||
} | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is how Z.filter
is derived.
var F = this.constructor; | ||
return this.chain(x => pred(x) ? F.of(x) : F.zero()); | ||
} | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is how Z.filterM
is derived.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This derivation precludes the use of takeWhile
as the Array#fantasy-land/filter
implementation, @paldepind.
README.md
Outdated
### Filterable | ||
|
||
A value that implements the Filterable specification must also implement | ||
the [Monoid](#monoid) specification. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the reason given in sanctuary-js/sanctuary-type-classes#37 I think Filterable should depend on Plus rather than Monoid. filter (odd) (Just (42))
should be valid.
In what way would Filterable be dependent on Monoid? Would it have a Monoid constraint?
Idempotence? xs.filter(p).filter(p).equals(xs.filter(p)) |
Thanks for the review, Gabe!
I'd been thinking that for a value to be filterable its type must have an After opening the pull request I remembered sanctuary-js/sanctuary-type-classes#37. Strictly speaking a I've updated the pull request's description to reflect the change from Monoid to Plus.
Good one! I've added this law. sanctuary-js/sanctuary-type-classes@master...davidchambers/filterable includes |
I think this good, i am just concerned why this should be depend on |
c070ce9
to
617e26c
Compare
@syaiful6, I would like to understand your objection to depending on Plus. Are you suggesting that there are types which could satisfy Filterable but not Plus? If so, I'm interested in seeing an example. I don't understand how |
@davidchambers there are types that can be filtered that not Functors or Monoids. This is why I believe the fundamental function is |
@davidchambers One thing that bugs me are, we want superclass Regarding the law, i am just asking rather than claiming. I think it useful to state whether the underlying structure can be changed or not. Also currently laws defined here are related to monoid laws, |
This was not what I had in mind. I couldn't think of an example of a type which can satisfy Filterable but does not have an “empty” value, so it seemed sensible to me for Filterable to require Plus. In addition to helping with laws, a Plus dependency may be helpful to readers thinking about which types can satisfy Filterable and which types cannot.
The type must not change, but the shape may change.
Interesting observation. It got me thinking we could add this law:
This property holds for several types. For example: > mfilter odd ([1,2,3] <|> [4,5,6])
[1,3,5]
> mfilter odd [1,2,3] <|> mfilter odd [4,5,6]
[1,3,5] But it does not hold for the Maybe type: > mfilter odd (Just 0 <|> Just 1)
Nothing
> mfilter odd (Just 0) <|> mfilter odd (Just 1)
Just 1 |
Interesting! Can you provide an example, @Fresheyeball, of a type which can support
In a Haskell context I agree that |
I was not the one asked, so excuse me for answering. It certainly is a tricky case 😄 But one case that I know of is containers that can only contain specific types or a constrained set of types. For instance something like an |
Thank you, Simon! This leads me to believe that Filterable should have no dependencies at all, which makes specifying laws quite challenging. 🤔 |
What would the type of this look like? Based on your description, I'd imagine it would not be polymorphic at all. I.e. if the keys are strings and the values are ints, there's no polymorphism. So of course a polymorphic
Any algebra we add will necessarily exclude some data types. If it didn't exclude some data types, it wouldn't be useful. Best not to worry too much over it. Just choose a formulation that makes sense, has laws that make the most sense, and let's move on. |
@davidchambers Sorry for not having reviewed this yet. I think there are enough people here discussing what's happening. I trust you all to make the right decision, so I'm going to remove my request for review. |
Yes, you are right 😄 I'm trying to describe a monomorphic type. I realize now that my example with
It doesn't have to be I think. Intuitively it seems like it should still be possible to filter values in a container even though it can only contain values of a single type. For instance, a string can be seen as a list that can only contain characters. And it might still be useful to filter characters based on a predicate. Something like But, you asked what the type of it would look like. That depends on the language 😉 In something like TypeScript it might look like this. Note that interface Filterable<A> {
filter(pred: (a: A) => boolean): Filterable<A>
}
class PolymorphicList<A> implements Filterable<A> {
constructor(private internalArray: A[]) { }
filter(pred: (a: A) => boolean): PolymorphicList<A> {
return new PolymorphicList(this.internalArray.filter(pred))
}
}
class IntList implements Filterable<number> {
constructor(private internalArray: number[]) { }
filter(pred: (a: number) => boolean): IntList {
return new IntList(this.internalArray.filter(pred))
}
} Basically the monomorphic type In Haskell, it is a bit more tricky (at least for me) but it can be done with multiparameter type classes and functional dependencies. Note that both the monomorphic {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE FlexibleInstances #-}
import Data.List as List
import Prelude hiding (filter)
class Filterable c a | c -> a where
filter :: (a -> Bool) -> c -> c
newtype IntList = IntList [Int]
deriving Show
-- Creating an instance for a monomorphic type
instance Filterable IntList Int where
filter f (IntList l) = IntList (List.filter f l)
-- Creating an instance for a polymorphic type
instance Filterable [a] a where
filter f l = List.filter f l |
@paldepind, it seems to me one could define Since Fantasy Land only supports polymorphic mapping it should only support polymorphic filtering.
This puts everything into perspective. Thank you for your sage comments, Hardy. :) |
Why? Is there a relationship between mapping and filtering? I think |
Yes. If this pull request is merged they'll both be specified here. 😉 We should follow the precedent set by |
I can't see the point of that. The intuition about Wouldn't a binary search tree (any flavor) be a good example of something clearly filterable but not mappable? Again, because of |
I think you misunderstood my point, Scott. I probably wasn't clear. I was trying to say that Fantasy Land does not allow |
I think I still don't understand it, David. Polymorphic types are implicit in If none of the FantasyLand specifications accepted monomorphic types, then I would see the argument, but of course plenty of them do. The integers with But I also simply don't see the connection. Is there a good reason why |
What does "implicit" mean here?
If we ever get over our hangup about defining data types, we can make the relationships clearer through |
Here is another law that I think is useful:
This law replaces the idempotence law since idempotence can be derived from it (by plugging in the same function for both |
Yes, that does seem to be a part of the disagreement. But please understand that I arrived at that definition by looking at any
Yes. I understand the problem and I agree with that. It it certainly not ideal! But, if we can make Filterable more useful by only eliminating |
This is very helpful. I now better understand your position (and realize that I went off at a tangent by discussing the meaning of the word in English). Thank you.
Every
As I mentioned in my earlier comment, I believe the derivations—I recently added a second—prevent the use of |
@@ -328,6 +329,32 @@ A value which has a Group must provide an `invert` method. The | |||
|
|||
1. `invert` must return a value of the same Group. | |||
|
|||
### Filterable | |||
|
|||
1. `v.filter(x => p(x) && q(x))` is equivalent to `v.filter(p).filter(q)` (distributivity) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This law actually Functor
laws, if you replace a -> Boolean
to monoid Conjunction
.
@masaeedu asked on Gitter whether Filterable could depend on Foldable. I can't think of a type which supports filtering but not folding, so this seems reasonable to me. We could then add a law which would please @paldepind and @CrossEye: |
If you do this, it might also be a good idea to reformulate the "annihilation" law in terms of |
Let's just pick the laws here, if you replace filter (const True) = id (identity)
filter (\x -> g x && f x) = filter f . filter g (composition) If we want this filter f mempty = mempty
filter f (x <> y) = filter f x <> filter f y if we pick this, |
@syaiful6 As mentioned in gitter, the thing I don't like about this set of laws is that it has nothing to say about |
Instead of just complaining about the current state I've thought about coming with some actual suggestions for a new name. What about calling the algebra Keep and the method
"keep" is both familiar and brand new. It makes sense as an english word but it doesn't come with the same bagage that "filter" has. This means that the word can guide people intuition about the algebra with about potentially being mislead by expecting too much from the "filter" name. Here is how "keep" makes sense for the A "normal"
Here is another method that satisfies the algebra. In english it "keeps everything if everything satisfies the predicate otherwise it keeps nothing". List.prototype.keep = function(pred) {
return this.all(pred) ? this : empty;
} What do you think about that name? I also think
Yes, you are right. I was thinking of a binary tree used to implement a set. That is a good example of something that is being excluded by adding the monoid law. To me that is the final blow to the idea of depending on monoid.
Thank you too. I think we understand each other better now. You where thinking about "filter" as an english word and I was thinking about existing
Ok. Then I think we agree that if we end up calling the specified method
Yes, I also think @CrossEye's argument is good. And I agree that it's not at all necessarily a bad thing that the algebra is more general. Do note thouhg that Scott's example, abstract algebra, definitely choose abstract names for their new abstract things (group, ring, fields) instead of reusing existing names. I too would like a new name for our new abstraction. But, if we can't find an appropriate new name then using "filter" is ok with me.
Correct me if I'm wrong but the derivations are a possible but not required way to implement methods?
This infinte list can be filtered but it cannot implement foldable since |
@paldepind It can only be filtered with lazy evaluation, you can't strictly evaluate a filtering of an infinite list for all possible predicates. E.g. you can't strictly evaluate |
Yes, that is my point. |
I'm not strongly attached to the name " |
A derivable method need not be derived, but its behaviour must be equivalent to that of the derivations:
|
3a341d4
to
59766a3
Compare
59766a3
to
0041cce
Compare
Having realized that the derivations preclude The laws are not perfect, but they may never be perfect. |
Actually, wikipedia deletion definition, which is not conmutative in some particular cases, implies that conmutativity law of filter also fails. Then, it seems that we should be careful about which implementation of filter is right, at least if we want to preserve the same traversal behaviour. In the same way I'm thinking about
I think it is not a counterexample, is a I can define another filter that does: String.prototype.filter = function(fn) {
return this.split('').filter(fn).join('');
} Demonstration: a.concat(b).filter(f)
// replace filter with its implementation, then
a.concat(b).split('').filter(f).join('')
// str1.concat(str2).split('') === str1.split('').concat(str2.split('')), then
a.split('').concat(b.split('')).filter(f).join('')
// concat-distirbutivity law works with Array type, then
a.split('').filter(f).concat(b.split('').filter(f)).join('')
// arrCh1.concat(arrCh2).join('') === arrCh1.join('').concat(arrCh2.join('')), then
a.split('').filter(f).join('').concat(b.split('').filter(f).join(''))
// replace filter implementation with filter method, then
a.filter(f).concat(b.filter(f)) The only thing that I can conclude is that there is some implementations that matches to anihilation, identity and &&-distributivity rule but not with concat-distributivity rule. However there are other implementations that do it.
Yes, this kind of argument convince me that maybe it isn't a good solution defining It seems that some partial solutions exists in the library. For example, you can implement this class: function Vector(numbers) {
this.numbers = numbers
}
Vector.prototype.sum = function(e) {
var [u, v] = this.numbers.length < e.numbers.length ? [this, e] : [e, this]
return new Vector(u.numbers.map((num, i) => num + v.numbers[i]))
}
Vector.prototype.map = function(fn) {
return new Vector(this.numbers.map(fn))
}
Vector.prototype.concat = function(e) {
return new Vector(this.numbers.concat(e.numbers))
} You can notice that Vector.prototype['fantasy-land/concat'] = Vector.prototype.sum
Vector.prototype['fantasy-land/map'] = Vector.prototype.map
Vector.prototype['fantasy-land/alt'] = Vector.prototype.concat Then, for this particular case, we can ensure that Therefore, in the same way that it can be used the |
Well spotted. Then we're back to the conclusion that the only thing ruled out by depending on Monoid is |
Hi! Thinking a litle bit more we can assume that:
And function contains(x) {
return this.filter(y => y is equivalent to x)
} Then, it seems that we can write some rule that avoids PS. It's not true. The function contains is wrong. |
That is a good idea. I've had similar thoughts myself including the exact same law and the idea of using contains. One approach would be to add Another approach would be to define contains conceptually by saying: A filterable In the end, I concluded that the approach wouldn't work. But maybe the above can give you further ideas? |
Well I was trying to show this as a reasonable example of something we might reasonably consider a
Think of Semigroups layered atop other types. From the math you learned, you might recall that a Semigroup is a set closed under an associative binary operation. The Semigroup is not just the set. It is the combination of the set with the operation. Thus you can have an Add Semigroup over the set of integers and a Multiply Semigroup over the set of integers, with no conflict. In programming, the set would be the collection of instances of a specific type, and to be a Semigroup, you would also need an associative binary operation. So, to this: "How I can define an algebra that has the same laws as integers?" the answer is simply that you can't. (If nothing else, think about Gödel.) But you can define several algebras on your type that map properly to some features of the integers. When we think of types like (I'm not following your |
Yes:
The problem I think that is in the implementation of deletion of node with only one child defined in wikipedia. It can be fixed with another definition.
Sorry, maybe I explained wrong my question or I can't understand your answer. I will be handier. I have this class: class Integer {
constructor(value) {
this.value = value;
}
sum(e) {
return new Integer(this.value + e.value)
}
mul(e) {
return new Integer(this.value * e.value)
}
} Regardless overflows, I can assume that given any three instances
Then:
My question is if I can define ({instances of Integer}, Integer.prototype.sum) as a FL semigroup and at the same time define ({instances of Integer}, Integer.prototype.mul) as a FL semigroup. As a FL semigroup, I mean a class that provides two methods with the same name: Integer.prototype['fantasy-land/concat'] = Integer.prototype.sum
Integer.prototype['fantasy-land/concat'] = Integer.prototype.mul It doesn't make sense for me. Then a FL semigroup and a semigroup are not the same (FL semigroup is more restrictive). But, why I'm interested in this question? Because @davidchambers says that if implementation of In my opinion, this is not a problem for avoiding to implement concat-distributivity for Filterable. We can define another operation
and the append-distributivity law:
In some types |
I was trying to point out that the laws under discussion don't explicitly state a commutative law. Because
I think the answer you're looking for is no. Obviously you can't define two different methods with the same name. But you can define the types The examples regarding the proposed |
funkia/list now implements Filterable (take that for what it's worth though as the library isn't ready for production yet (it's getting there though)). |
That's great, Simon! I have released [email protected] which includes the lovely new |
Closes #33
While working on this pull request I consulted both Data.Witherable and Control.Compactable. Neither definition is appropriate for Fantasy Land due to our decision not to rely on data types not provided by the language. We must use
a -> Boolean
rather thana -> Maybe a
.While reviewing this pull request please consider the following questions:
MonoidPlus?I'm requesting a review from you, @joneshf, as way back in sanctuary-js/sanctuary#10 you planted the seed that resulted in this pull request. 🌱
/cc @Fresheyeball