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

add Filterable algebra #274

Merged
merged 1 commit into from
Jan 6, 2018
Merged

Conversation

davidchambers
Copy link
Member

@davidchambers davidchambers commented Oct 8, 2017

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 than a -> Maybe a.

While reviewing this pull request please consider the following questions:

  • Should Filterable depend on any type classes other than Monoid Plus?
  • Can we state any other laws?

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

@davidchambers davidchambers requested a review from joneshf October 8, 2017 21:32
README.md Outdated
var F = this.constructor;
return this.reduce((f, x) => pred(x) ? f.concat(F.of(x)) : f, F.empty());
}
```
Copy link
Member Author

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());
}
```
Copy link
Member Author

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.

Copy link
Member Author

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.
Copy link
Member Author

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.

@gabejohnson
Copy link
Member

gabejohnson commented Oct 9, 2017

Should Filterable depend on any type classes other than Monoid?

In what way would Filterable be dependent on Monoid? Would it have a Monoid constraint?

Can we state any other laws?

Idempotence?

xs.filter(p).filter(p).equals(xs.filter(p))

@davidchambers
Copy link
Member Author

Thanks for the review, Gabe!

Should Filterable depend on any type classes other than Monoid?

In what way would Filterable be dependent on Monoid? Would it have a Monoid constraint?

I'd been thinking that for a value to be filterable its type must have an empty value. filter(K(false), Just(0)) can evaluate to Nothing but filter(K(false), Right(0)) is a type error.

After opening the pull request I remembered sanctuary-js/sanctuary-type-classes#37. Strictly speaking a filter based on Monoid would permit Just(x) to be filtered only if x satisfies the requirements of Semigroup. Maybe a satisfies Semigroup only if a satisfies Semigroup. Were Filterable to depend on Monoid (for empty), which in turn depends on Semigroup, only “concatenatable” Maybe values would be filterable. So, Filterable should depend on Plus (for zero) instead.

I've updated the pull request's description to reflect the change from Monoid to Plus.

Can we state any other laws?

Idempotence?

xs.filter(p).filter(p).equals(xs.filter(p))

Good one! I've added this law.

sanctuary-js/sanctuary-type-classes@master...davidchambers/filterable includes filter implementations for Array a, StrMap a, List a, and Maybe a. It replaces the differently derived filter and filterM functions with a single filter function which is likely to be more efficient.

@syaiful6
Copy link

syaiful6 commented Oct 12, 2017

I think this good, i am just concerned why this should be depend on Plus? Maybe just require it to Functor? and i think the most generic one just use partition :: (a -> Boolean) -> f a -> { no :: f a, yes :: f a }, that way you can have laws that stated this connection with Functor?

@davidchambers davidchambers force-pushed the filterable branch 3 times, most recently from c070ce9 to 617e26c Compare October 14, 2017 14:00
@davidchambers
Copy link
Member Author

@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 partition would allow us to define a law. Could you clarify this for me?

@Fresheyeball
Copy link

Fresheyeball commented Oct 18, 2017

@davidchambers there are types that can be filtered that not Functors or Monoids. This is why I believe the fundamental function is compact :: Compactable f => f a -> a. We can define filter in terms of compact without and dependency.

@syaiful6
Copy link

syaiful6 commented Oct 18, 2017

@davidchambers Either l r is one of data type that don't have Plus instance i think, but it can implements Filterable if the constraint just a Functor, but Either instance for Filterable isn't really useful.

One thing that bugs me are, we want superclass Plus so we can use alt for appending, but that's not always the case. The derived example use chain and pure, but Monad isn't superclass of Filterable, pure is used as singleton, and chain operation is concatMap, so it will only work for list like structure. if you trying to generalize filter using Foldable, Applicative and Monoid/Plus, it will fail if you apply it to ZipList since the pure method of ZipList isn't singleton..

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, filter f (a + b) = filter f a + filter f b etc.

@davidchambers
Copy link
Member Author

we want superclass Plus so we can use alt for appending

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.

I think it useful to state whether the underlying structure can be changed or not.

The type must not change, but the shape may change. filter(odd, Just(0)) should evaluate to Nothing, for example.

Also currently laws defined here are related to monoid laws, filter f (a + b) = filter f a + filter f b etc.

Interesting observation. It got me thinking we could add this law:

  • f.alt(g).filter(p) is equivalent to f.filter(p).alt(g.filter(p)) (distributivity)

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

@davidchambers
Copy link
Member Author

@davidchambers there are types that can be filtered that not Functors or Monoids.

Interesting! Can you provide an example, @Fresheyeball, of a type which can support filter but not map? I have not been able to think of one.

This is why I believe the fundamental function is compact :: Compactable f => f a -> a.

In a Haskell context I agree that compact is the best choice. In the context of Fantasy Land we don't have the luxury of referencing the Maybe type, so specifying filter is the way to go. If and when this pull request is merged we could add compact to Sanctuary (since it provides a Maybe implementation), defined in terms of filter. :)

@paldepind
Copy link

@davidchambers

Interesting! Can you provide an example, @Fresheyeball, of a type which can support filter but not map? I have not been able to think of one.

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 IntMap, i.e. a map that can only contain integers. Such a structure cannot be a functor because a functor must be able to contain whatever type the mapping function returns. But it can easily implement filter. A concrete example from Haskell is Data.Set which has filter but cannot implement Functor since it can only contain types that implement Ord.

@davidchambers
Copy link
Member Author

Thank you, Simon! This leads me to believe that Filterable should have no dependencies at all, which makes specifying laws quite challenging. 🤔

@joneshf
Copy link
Member

joneshf commented Oct 25, 2017

For instance something like an IntMap, i.e. a map that can only contain integers. Such a structure cannot be a functor because a functor must be able to contain whatever type the mapping function returns. But it can easily implement filter.

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 map is out, but a polymorphic filter would also be out.

Thank you, Simon! This leads me to believe that Filterable should have no dependencies at all, which makes specifying laws quite challenging.

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.

@joneshf
Copy link
Member

joneshf commented Oct 25, 2017

@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.

@joneshf joneshf removed their request for review October 25, 2017 12:59
@paldepind
Copy link

@joneshf

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.

Yes, you are right 😄 I'm trying to describe a monomorphic type. I realize now that my example with IntMap was confusing 😕 It could also refer to a map/dictionary where the keys would be restricted to integers but where the values could still be polymorphic.

So of course a polymorphic map is out, but a polymorphic filter would also be out.

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 filter(isVowed, aString). As another example, a set type may only be able to store values that can be sorted but being able to implement filter on it still seems like something that would be possible.

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 Filterable is implemented both by a polymorphic and a monomorphic type .

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 IntList implements only Filterable<number> whereas the polymorphic list can implement Filterable<A> for all A.

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 IntList and the normal polymorphic list implements the Filterable type class below.

{-# 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

@davidchambers
Copy link
Member Author

@paldepind, it seems to me one could define map for IntList with the function argument constrained to (Integer -> Integer) just as easily as one could define filter for IntList with the function argument constrained to (Integer -> Boolean).

Since Fantasy Land only supports polymorphic mapping it should only support polymorphic filtering.

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.

This puts everything into perspective. Thank you for your sage comments, Hardy. :)

@paldepind
Copy link

@davidchambers

Since Fantasy Land only supports polymorphic mapping it should only support polymorphic filtering.

Why? Is there a relationship between mapping and filtering?

I think Filterable as you've currently specified it in the PR looks really good. Depending on Plus gives the annihilation law which seems both sensible and practically useful.

@davidchambers
Copy link
Member Author

Is there a relationship between mapping and filtering?

Yes. If this pull request is merged they'll both be specified here. 😉

We should follow the precedent set by map, which is defined only for types which can support (a -> b) as the first argument. map could be defined to accommodate monomorphic mapping, but currently it is not. Changing map, filter, and possibly other methods to accommodate monomorphic types is beyond the scope of this pull request.

@CrossEye
Copy link

We should follow the precedent set by map, which is defined only for types which can support (a -> b) as the first argument.

I can't see the point of that. filter should know only about the one type.

The intuition about filter is that it keeps only those items for which the predicate returns true. There is no b in a -> Boolean. 😄

Wouldn't a binary search tree (any flavor) be a good example of something clearly filterable but not mappable? Again, because of Ord. Any reasonable definition of filter should be able to accept that in my book.

@davidchambers
Copy link
Member Author

I think you misunderstood my point, Scott. I probably wasn't clear. I was trying to say that Fantasy Land does not allow map to be defined for monomorphic types such as IntMap. In order to remain consistent, it should also not allow filter to be defined for monomorphic types such as IntMap either.

@CrossEye
Copy link

I think I still don't understand it, David.

Polymorphic types are implicit in a -> b. But they are not implicit in a -> Bool.

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 + certainly forms a Semigroup, and if you add in 0, it's a Monoid. That's monomorphic.

But I also simply don't see the connection. Is there a good reason why filter should have any connection at all to map?

@joneshf
Copy link
Member

joneshf commented Oct 26, 2017

Polymorphic types are implicit in a -> b. But they are not implicit in a -> Bool.

What does "implicit" mean here?

Is there a good reason why filter should have any connection at all to map?

  • filter is a more restricted filterMap, which is a more restricted partitionMap.
  • filterMap and map have a direct relation: map(f) = filterMap((x) => Just(f(x))).
  • filter and filterMap have a direct relation: filter(f) = filterMap((x) => f(x) ? Just(x) : Nothing).
  • filter and map have an indirect relation: filter(const(true)) = map(id).

If we ever get over our hangup about defining data types, we can make the relationships clearer through partitionMap.

@paldepind
Copy link

Here is another law that I think is useful:

f.filter(p).filter(q) is equivalent to f.filter(x => p(x) && q(x))

This law replaces the idempotence law since idempotence can be derived from it (by plugging in the same function for both p and q).

@paldepind
Copy link

@davidchambers

We appear to have incompatible definitions of filtering. Your definition, Simon, seems to require the removal of 100% of undesirable material and the retention of 100% of desirable material. I prefer a looser definition. I consider straining pasta to be a filtering operation even though a few pieces of spaghetti could slip through holes in the colander. 🍝

Yes, that does seem to be a part of the disagreement. But please understand that I arrived at that definition by looking at any filter function or method "in the wild" that I could find and by describing what they all had in common. I don't think I've ever seen a function anywhere called "filter" that didn't satisfy my definition. If you can show real world examples of programmers using the name "filter" in a more general sense than mine then I'll happily change my understanding of "filter" to yours. Otherwise, I'll keep claiming that calling an algebra that includes takeWhile as an instance by the name filter is misleading.

The thing is that Maybe#fantasy-land/concat has useful behaviour.

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 Maybe then I think it is worth it. Maybe doesn't have the most terribly interesting filter method anyway and a Maybe can easily be converted into a list which can implement filterable.

@davidchambers
Copy link
Member Author

But please understand that I arrived at that definition by looking at any filter function or method "in the wild" that I could find and by describing what they all had in common.

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.

I don't think I've ever seen a function anywhere called "filter" that didn't satisfy my definition.

Every filter function I have so far encountered matches your expectations, which I share. Perhaps, though, my expectations are unnecessarily exacting. I find this passage from @CrossEye compelling:

But more likely, the laws may end up widening our view of what they describe. Much of abstract algebra […] grew out of observations about numbers and modular arithmetic. That they apply to many other topics is not an issue; it's a revelation. It demonstrates deep interconnections between disparate subjects.

As I mentioned in my earlier comment, I believe the derivations—I recently added a second—prevent the use of takeWhile for Array#fantasy-land/filter. Hopefully this resolves the final sticking point. Are you happy, Simon, for TakeWhileList#fantasy-land/filter to provide a different type of filtering?

@@ -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)
Copy link

@syaiful6 syaiful6 Jan 4, 2018

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.

@davidchambers
Copy link
Member Author

@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:

  • size(filter(p, v)) + size(filter(x => !p(x), v)) is equivalent to size(v) (totality)

@masaeedu
Copy link

masaeedu commented Jan 4, 2018

If you do this, it might also be a good idea to reformulate the "annihilation" law in terms of size: compose(size, filter(const(false))) === const(0). This avoids "cheat" definitions of filterables that just always return the same value and vacuously satisfy the current law.

@syaiful6
Copy link

syaiful6 commented Jan 4, 2018

Let's just pick the laws here, if you replace a -> Boolean to Conjunction monoid, then filter will form functor laws:

filter (const True) = id (identity)
filter (\x -> g x && f x) = filter f . filter g (composition)

If we want this Filterable depend to Monoid, filter also form monoid morphism:

filter f mempty = mempty
filter f (x <> y) = filter f x <> filter f y

if we pick this, takeWhile should fail to satisfy this laws i think. The lose here only type that form Monoid are able to implements Filterable.

@masaeedu
Copy link

masaeedu commented Jan 4, 2018

@syaiful6 As mentioned in gitter, the thing I don't like about this set of laws is that it has nothing to say about Filterables implemented as const filter = p => x => x. In fact this is a lawful implementation for every possible filterable under the two laws given, so as a consumer this don't tell me anything useful about how a filterable will behave.

@paldepind
Copy link

paldepind commented Jan 4, 2018

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. I like keep because

  • The english word "keep" makes intuitive sense for the various methods that can implement the algebra
  • Unlike filter the name "keep" does not have an established meaning that is more specific than the method in this algebra.

"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 filter and takeWhile instance.

A "normal" filter is essentially "keep everything satisfying the predicate": keep(odd, [1,2,3]) => [1,3]

  • takeWhileRight and takeWhileLeft keeps elements from an end that satisfies the predicate. keep(odd, [1,2,3]) => [1]

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 take is a good option but that word is already used for a different thing in Lodash, Ramda and Haskell's prelude.

@CrossEye

I don't think so. What if the type implements preorder and postorder traversals as well as the inorder one we're assuming?

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.

@davidchambers

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.

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 filter functions.

Every filter function I have so far encountered matches your expectations, which I share.

Ok. Then I think we agree that if we end up calling the specified method filter it wouldn't be completely off if we think of the english word "filter" but it would go against what filter normally implies in programmer's code.

Every filter function I have so far encountered matches your expectations, which I share. Perhaps, though, my expectations are unnecessarily exacting. I find this passage from @CrossEye compelling:

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.

@davidchambers

As I mentioned in my earlier comment, I believe the derivations—I recently added a second—prevent the use of takeWhile for Array#fantasy-land/filter.

Correct me if I'm wrong but the derivations are a possible but not required way to implement methods?

I can't think of a type which supports filtering but not folding, so this seems reasonable to me.

This infinte list can be filtered but it cannot implement foldable since reduce is strict. An FRP stream can be filtered but cannot be folded. Any structure that contains values in an asynchronous-like way can be filtered but it cannot be folded since that would require all values to exist at one moment in time.

@masaeedu
Copy link

masaeedu commented Jan 4, 2018

@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 filter(const(true)) over an infinite list.

@paldepind
Copy link

@masaeedu

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 filter(const(true)) over an infinite list.

Yes, that is my point. filter can be implemented lazily but reduce from foldable is strict so the infinite list can be filtered but not reduced.

@CrossEye
Copy link

CrossEye commented Jan 4, 2018

I'm not strongly attached to the name "filter". I would prefer it, but would not object to "keep" or something similar. But there is one more argument in favor of it in context of FantasyLand: especially at the beginning, FL took pains to use names that already worked with existing Javascript types: "concat" rather than "append", "reduce" rather than "fold", "map" rather than "fmap", etc. It makes sense to me to continue with that here, since we already have Array.prototype.filter.

@davidchambers
Copy link
Member Author

As I mentioned in my earlier comment, I believe the derivations—I recently added a second—prevent the use of takeWhile for Array#fantasy-land/filter.

Correct me if I'm wrong but the derivations are a possible but not required way to implement methods?

A derivable method need not be derived, but its behaviour must be equivalent to that of the derivations:

If a data type provides a method which could be derived, its behaviour must be equivalent to that of the derivation (or derivations).

@davidchambers
Copy link
Member Author

Having realized that the derivations preclude Array#fantasy-land/filter from behaving like takeWhile, I'm now comfortable merging this pull request.

The laws are not perfect, but they may never be perfect.

@davidchambers davidchambers merged commit c31ea80 into fantasyland:master Jan 6, 2018
@davidchambers davidchambers deleted the filterable branch January 6, 2018 20:26
@xgbuils
Copy link
Contributor

xgbuils commented Jan 6, 2018

@CrossEye

a.concat(b).filter(f) is equivalent to a.filter(f).concat(b.filter(f))

For a fairly straightforward binary search tree, I think this would fail:

That is the same tree with two different internal representations.
I don't think so. What if the type implements preorder and postorder traversals as well as the inorder one we're assuming?

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 concat and filter for binary search tree. It is needed a definition that matches to the rules.

a.concat(b).filter(f) is equivalent to a.filter(f).concat(b.filter(f))

I have a simpler counterexample:

String concatenation with the empty String is a clear monoid. This seems reasonable:

String.prototype.filter = function(fn) {return fn(this) ? this : '');

I think it is not a counterexample, is a filter definition that applies anihilation, identity and &&-distributivity rule, but doesn't apply concat-distributivity rule.

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.

@davidchambers

The thing is that Maybe#fantasy-land/concat has useful behaviour. Maybe a satisfies Semigroup if a satisfies Semigroup. Haskell and Sanctuary both allow concat (Just ("foo")) (Just ("bar")) (although the function is named mappend in Haskell). It evaluates to Just ("foobar") by applying concat/mappend to the inner values then wrapping the result.

It would be a shame to be forced to choose between concat and filter. We should ensure that values of type Maybe a can be compatible with both functions.

Yes, this kind of argument convince me that maybe it isn't a good solution defining concat to returning always Nothing. However this problem makes me think that the library can't reflect the algebra like I learnt. I mean, for example, I know that integer numbers are a semigroup with the plus operator but also, with the multiplication operator. However, in fantasy land we have just one method(fantasy-land/concat) to define a semigroup. Then, how I can define an algebra that has the same laws as integers?

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 sum and concat are operations which satisfy Semigroup laws. Then, what method should I define as fantasy-land/concat? However, in this case, I know that exists an Alt algebra that has a method fantasy-land/alt which behaves like a semigroup (associativity law). Further, Alt implements Functor and distributivity law between fantasy-land/alt and fantasy-land/map. Then, it could be interesting define Vector in this way:

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 concat and sum are semigroups over Vector type. I don't think that we can do the same with Integer type without forcing to be a Functor (having map method). By the way, this is not the topic of this PR. It's just an example to show that we can implement a type that has 2 methods that satisfy the semigroup laws.

Therefore, in the same way that it can be used the fantasy-land/alt and fantasy-land/map to define an algebra that is a semigroup (alt behaves like a semigroup) and also has a distributivity law without force to implement the Semigroup Fantasy Land algebra. We can do the same with filter, defining a fantasy-land/union method that behaves like a semigroup and has a distributibity law with fantasy-land/filter. . In this way, for Maybe, we can define fantasy-land/union as method that returns Nothing. However you can use fantasy-land/concat to define a method that works for a Maybe of a Fantasy Land Semigroup.

@davidchambers
Copy link
Member Author

@paldepind
Copy link

@xgbuils

Actually, wikipedia deletion definition, which is not conmutative in some particular cases, implies that conmutativity law of filter also fails

Well spotted. Then we're back to the conclusion that the only thing ruled out by depending on Monoid is Maybe. And the laws with monoid really are both more elegant and much stronger and thus more useful.

@xgbuils
Copy link
Contributor

xgbuils commented Jan 7, 2018

Hi!

Thinking a litle bit more we can assume that:

a.contains(x) && p(x) === true if and only if a.filter(p).contains(x)

And contains is a particular case of filter:

function contains(x) {
     return this.filter(y => y is equivalent to x)
}

Then, it seems that we can write some rule that avoids takeWhile and allows Maybe based just on filter method.

PS. It's not true. The function contains is wrong.

@paldepind
Copy link

@xgbuils

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 contains as a method to the algebra. But that has problems similar to depending on Foldable. Infinite lists and FRP streams cannot implement contains.

Another approach would be to define contains conceptually by saying: A filterable f contains the element x if p is a predicate that returns true only for x and f.filter(p) is not equal to f.filter(_ => false). I initially thought that would work but the problem is that when contains is defined based on filter a "broken" filter will also result in a broken contains and it can still satisfy the law.

In the end, I concluded that the approach wouldn't work. But maybe the above can give you further ideas?

@CrossEye
Copy link

CrossEye commented Jan 7, 2018

@xgbuils:

String concatenation with the empty String is a clear monoid. This seems reasonable:

String.prototype.filter = function(fn) {return fn(this) ? this : '');

I think it is not a counterexample, is a filter definition that applies anihilation, identity and &&-distributivity rule, but doesn't apply concat-distributivity rule.

I can define another filter that does: ...

Well I was trying to show this as a reasonable example of something we might reasonably consider a Filterable to demonstrate that the proposed concat rule is too restrictive. That was the same point for the search tree. (And what "commutative" rule did this violate, a derivation of the "distributive" one?)


I know that integer numbers are a semigroup with the plus operator but also, with the multiplication operator. However, in fantasy land we have just one method(fantasy-land/concat) to define a semigroup. Then, how I can define an algebra that has the same laws as integers?

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 Maybe, they are generally simple enough that we can define particular operations and not really think of alternatives, but often even those simple ones are hiding alternatives: we might have a different concat or some such for these types that don't match the canonical ones but are still perfectly legal. Choosing a useful one is an art, not a science.


(I'm not following your contains example at all. Can you elaborate?

@xgbuils
Copy link
Contributor

xgbuils commented Jan 9, 2018

@CrossEye

(And what "commutative" rule did this violate, a derivation of the "distributive" one?)

Yes:

        2                        2                              4
       / \                        \                            /
      1   4 filter(isNotOne) -->   4 --> filter(isNotTwo) --> 3
         /                        /
        3                        3

        |
       \|/               3                            3
                        / \                            \
  filter(isNotTwo) --> 1   4 --> filter(isNotOne) -->   4

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.

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.

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 a, b and c of Integer we have:

  1. a.sum(b).sum(c) is equivalent to a.sum(b.sum(c))
  2. a.mul(b).mul(c) is equivalent to a.mul(b.mul(c))

Then:

  1. ({instances of Integer}, Integer.prototype.sum) is a semigroup.
  2. ({instances of Integer}, Integer.prototype.mul) is a semigroup.

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: fantasy-land/concat but behaves different

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 fantasy-land/concat is a method which always returns Nothing, then, we can't implement more useful fantasy-land/concat for Maybe a such that a is instance of a Semigroup.

In my opinion, this is not a problem for avoiding to implement concat-distributivity for Filterable. We can define another operation fantasy-land/union or fantasy-land/append that satisfies the associative law:

  1. a.append(b).append(c) is equivalent to a.append(b.append(c))

and the append-distributivity law:

  1. a.append(b).filter(f) is equivalent to a.filter(f).append(b.filter(f))

In some types fantasy-land/append and fantasy-land/concat could be the same. In other types, like Maybe, couldn't.

@CrossEye
Copy link

(And what "commutative" rule did this violate, a derivation of the "distributive" one?)

Yes [... example elided ...]

I was trying to point out that the laws under discussion don't explicitly state a commutative law. Because && is commutative for booleans, we can probably derive one from the distributive law.

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.

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 Sum and Product which each take an Integer value and have concat methods invoking addition and multiplication, respectively. You can also, of course define concat on an Integer type to match one of them or based on any other associative operation. I don't recommend this, but it's legal.


The examples regarding the proposed concat law all had to do with showing things that seem to be reasonable Filterable types but for which that law would fail. That is why I didn't want to include it.

@paldepind
Copy link

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)).

@davidchambers
Copy link
Member Author

That's great, Simon!

I have released [email protected] which includes the lovely new filter and its complement, reject. I have also opened sanctuary-js/sanctuary#475 to add these functions to Sanctuary and to make Sanctuary's Maybe and Either types compatible with the new type class.

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

Successfully merging this pull request may close these issues.

10 participants