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

[Question] Strategy for using HKT with classes we can not touch. #13

Open
clinuxrulz opened this issue May 6, 2016 · 19 comments
Open

Comments

@clinuxrulz
Copy link

Not sure if this is the right place for this question.

But is there a way to easily handle using HKT for classes we can not extend/implement __, __2, etc. for. Without writing a bunch of wrapper classes.

For example, using HKT with Java 8's Stream class.

@clinuxrulz
Copy link
Author

clinuxrulz commented May 6, 2016

I was thinking something like this:

    public static class _w<M,A> implements __<_w.µ<M>,A> {
        public static class µ<F> {}
        private final M inner;
        private _w(M inner) { this.inner = inner; }
        public static <M,A> _w<M,A> wrap(M inner) { return new _w<>(inner); }
        public M unwrap() { return inner; }
    }

But that still requires someone to write wrappers for stuff they wanna use:

    public static class WStream {
        public static <M,A> _w<Stream<?>,A> wrap(Stream<A> inner) { return _w.wrap(inner); }
        public static <M,A> Stream<A> unwrap(_w<Stream<?>,A> w) { return (Stream<A>)w.unwrap(); }
    }

@jbgi
Copy link
Member

jbgi commented May 6, 2016

this is a very valid question and we need support or at least a story for this.
I think we should provide a hkt-std with wrappers for all jdk generic classes.
Now should we use µ<F> or a dedicated wrapper for each class... Also if we wrap a known set of classes, we can theoretically avoid casting and use a visitor instead.

@clinuxrulz
Copy link
Author

That sounds fair. I'll close this for now.

@gneuvill
Copy link
Contributor

gneuvill commented May 6, 2016

Also if we wrap a known set of classes, we can theoretically avoid casting and use a visitor instead

Can we ? Would you mind giving an example of that cause I don't understand what you allude to ?

@jbgi
Copy link
Member

jbgi commented May 6, 2016

@gneuvill I'm talking about the approach described in the first comment of functionaljava/functionaljava#126 (comment)
But now that we have the processor there is little point in it.

@johnmcclean
Copy link

Hey guys -

I know this issue is closed, but it seems the best place to pose these questions. I've been playing around with making this work with 3rd party classes, example possible API below -

 Functor<ListType.µ> f = TypeClasses.General
                                    .<ListType.µ,List<?>>functor(ListType::narrow,(list,fn)->ListX.fromIterable(list).map(fn));

List<Integer> mapped2 = f.map(a->a+1, ListType.widen(Arrays.asList(1,2,3)))
                         .apply_(f::map,λ(this::mult3))
                         .apply_(f::map,λ(this::add2))
                         .convert(ListType::narrow);

Based on a ListType interface & wrapping implementation. If it's an interface, your own Lists can implement that interface (and as ListType extends List it must also be a List - I think if we were wrapping a class rather than an interface we should have an abstract class that extends that class instead - what do you guys think?).

public interface ListType<T> extends  Higher<ListType.µ,T>, List<T>,__<ListType.µ,T>{

    public static class µ{}
    static final class Box<T> implements ListType<T> {
       //wrapper
    }
}

The Higher interface is a place for putting methods to create a fluent API at the moment. But I also think it would be great to have more meaningful semantic alias' also. It would be great to have longer alias' that extend __, __2 etc in the HKT project - what do you think?

public interface Higher<T1,T2> extends Convert<Higher<T1,T2>>,__<T1,T>{


    default <T3,R> Higher<T1,R> apply_(BiFunction<T3,Higher<T1,T2>,Higher<T1,R>> biFn,T3 param ){
        return biFn.apply(param,this);
    }

}

@jbgi
Copy link
Member

jbgi commented Nov 2, 2016

A first step would be to modify the annotation processor such that user-defined alias like Higher<T1,T2> extends __<T1,T> are permitted: I'd rather this project does not provide its own aliases if possible.
Alternatively, we could change the name of __ interfaces to have more meaning (eg. HigherK) but this would need to be validated by @DanielGronau, @clinuxrulz and @gneuvill.

I like the idea of fluent default methods, maybe we could some of them to the main hkt interfaces?

As for ListType I think it can be a final class (no need for the Box class I think), no? What are the reasons one would want to extends a ListType interface?
(btw, I would prefer the name ListK, but this is just me)

@johnmcclean
Copy link

johnmcclean commented Nov 2, 2016

It looks like you can create your own Alias's as long as they defined in the same package as __, __2 etc. But a less 'hacky' way for client libraries would also be good.

There may be downsides in switching to a more verbose form everywhere (method signatures of some functions in HighJ would become very long). My thinking on the alias's was roughly this

  • Using underscores as types is something alien to typical Java practices. Which would make general acceptance of the concept that much harder.
     __<List.µ,Integer>
     HigherK<List.µ,Integer>

The later looks more like standard Java.

  • Generic type signatures would be too verbose to use semantic names everywhere
     HigherK<List.µ,__<List.µ,Integer>>

With HigherK extending __, API developers could specify HigherK as the first type parameter in a nested set and use the more concise form thereafter (which should psychologically benefit the reader).

FluentTypes Sure, wasn't sure if you'd want them or not. The potential downside of adding them at the top of the hierarchy is the risk of collision with methods on other types.

ListK I presume this is short for ListKind? Would ListKind itself be good in that case? I would expect it to be less frequently used in full form, so providing the full semantic name might help with general understanding / acceptance too.

Thinking behind why ListK should be an interface is that java.util.List is an interface. ListK is just the Higher Kinded Encoding of a java.util.List - splitting java List<T> into ListK.µ,T. The ListK.µ,T is still a Java List, just one we can abstract over at a higher level. I think API designers may want to implement ListK / ListKind directly rather than java.util.List. We have ListX (SetX, DequeX etc) in cyclops-react, I'd like them to implement it. Then they could be used directly with HighJ without boxing / wrapping.

If ListK extends List then it isn't possible to break the type safety of ListK by implementing it on something that isn't a List.

What do you think?

@jbgi
Copy link
Member

jbgi commented Nov 2, 2016

@johnmcclean-aol if you make ListX extends ListK then it means that you won't be able to recover a ListX instance from its higher kinded encoding (only a ListK). I would think this may be a problem.

@johnmcclean
Copy link

johnmcclean commented Nov 2, 2016

True. You can't recover it directly. ListX (and all the extended collections) might be a special case though because they are wrappers / boxes themselves. They all wrap a collection type so ListX accepts java.util.List which ListK is. So this would work ->

public static <T> ListX<T> narrow(final Higher<ListK.µ, T> list) {
        return ListX.fromIterable(ListK.narrow(list));
}

ListX.fromIterable won't nest a ListX inside a ListX (does a runtime type check for ListX), but would wrap / box any other List impl (including any ListK impl).

It's probably not an issue if you create a new List implementation (I mean implement java.util.List only) as 99% of the time the left hand side of List assignments are the abstract type.

      List<Integer> list = new ArrayList<>();

So if you are just implementing java.util.List I think this would work also...

public class MyNewListImpl<T> implments ListK<T> { ...  }

In practice recovering ListK is probably ok

      ListK<Integer> list = new MyNewListImpl<>();

I think the downside of this approach would be felt if someone extended java.util.List and then implemented that extended type not as a wrapping implementation. But there are at least two use cases where it seems to work on.

I think there are other advantages as well.

e.g. It's impossible to convert mappedX below back to StreamK<ListK<Integer>> without doing a transformation operation on it (or casting), as far as I can see (maybe with Leibniz equality you guys describe here #17, but that is also not currently available on __<f,t> or I don't know how to use it :) )

Higher<StreamK.µ,Higher<ListK.µ,Integer>> mappedX = f.map(a->ListX.<Integer>of(a+1), ReactiveSeq.of(1,2,3));

//compile error
Stream<List<Integer>> mapped8 =  mappedX.convert(StreamType::narrow);

But this works

Higher<StreamType.µ,MaybeType<Integer>> mapped3 = f.map(a->MaybeType.<Integer>just(a+1), ReactiveSeq.of(1,2,3));

ReactiveSeq<MaybeType<Integer>> mappeda =  mapped3.convert(StreamType::narrowReactiveSeq);
ReactiveSeq<Maybe<Integer>> mappedb =  mapped3.convert(StreamType::narrowReactiveSeq);
  • Note Maybe doesn't implement MaybeType so to avoid 'widening' on Maybe creation I've added creational methods to MaybeType. It won't avoid it everywhere though.

Which means that there is some advantage to creating the more verbose type specific Kind interfaces (avoiding transformation and Leibniz coercion) in general. And you get the benefit of avoiding the widening operation if your custom types implement those specific Kind interfaces (or extend from it where that makes sense also).

It definetely should not be done for types such as Optional (which already is a final class) because OptionalK can't also be an Optional. But ListK can be a List and it seems to be pretty useful (in my investigations so far) to be able to implement ListK.

Are there any downsides to allowing clients to implement Kinds?

@johnmcclean
Copy link

^^^ How I spent my halloween weekend by the way :)

@clinuxrulz
Copy link
Author

clinuxrulz commented Nov 3, 2016

This change makes me nervous:

__<List.µ,Integer>
HigherK<List.µ,Integer>

There might be times when you are writing code that operates with an unknown monad "M". And with that change everything suddenly becomes a lot more verbose. But on the other hand, the average Java developer probably does not work at that level of abstraction.

In my code base i have something like:
public static <M> __<M,Unit> doSomethingInScene(MonadRec<M> monadRec, SceneEffects<M> sceneEffects, DoSomethingParams doSomethingParams);

This allows multiple underlying implementation of the Scene to construct graphs. One can use immutable maps (great for tests), and one can use mutable maps (great for performance), without any change to the rest of the code base.

Also working with an unknown monad does not require the programmer to know if they are working with asynchronous or synchronous effects too. The same linear (sequential like) flow continues whether or not we touch the cloud or a database.

@clinuxrulz
Copy link
Author

clinuxrulz commented Nov 3, 2016

@jbgi I really like your closed union approach for 3rd party classes functionaljava/functionaljava#126 (comment).

I wonder if there are any remaining tricks we can use to go from "Closed Union" to "Open Union". Like somehow treating Types<R, X, T> from your closed world impl as a type-class (java interface).

Edit: Actually I don't think there is a way to go to "Open Union". I keep thinking open union would work, then realize it doen't work. 😏

@clinuxrulz clinuxrulz reopened this Nov 3, 2016
@jbgi
Copy link
Member

jbgi commented Nov 3, 2016

^^^ How I spent my halloween weekend by the way :)

Yeah, higher-kinded types in Java are scary, so perfect for an halloween weekend! ;)

About the aliases: let's keep __<> as is. I will make modifications to the hkt processor in the next days to properly handle user-defined aliases.

@johnmcclean
Copy link

@jbgi Excellent - thanks!

@clinuxrulz
Copy link
Author

Good 😅

I'd much prefer

__<__<__<StateT.µ,MyState>,M>,Unit> as an encoding for StateT<MyState,M,Unit>

compared to

HigherK<HigherK<HigherK<StateT.µ,MyState>,M>,Unit> as an encoding for StateT<MyState,M,Unit>

If i need to add state while working with an unknown monad 😄

I think its better to make things look close to how they are ment to be if Java did handle higher kinded types.

@gneuvill
Copy link
Contributor

gneuvill commented Nov 3, 2016

@jbgi

I will make modifications to the hkt processor in the next days to properly handle user-defined aliases.

Just a heads-up to say that I've finally found the time to work on #11 . It's not finished, but definitely wip. Don't know if that would clash with your intended work though.

@jbgi
Copy link
Member

jbgi commented Nov 3, 2016

@gneuvill great, I will wait for it then.

@clinuxrulz
Copy link
Author

clinuxrulz commented Nov 3, 2016

On a side note there has been proposals put forth for do-notation in C++17. This has been proposed when they've realized how well Haskell has handled asynchronous effects for all these years. (excape from call back hell)

Maybe do-notation and Monads will be mainstream for Java one day too.

See: http://www.hyc.io/boost/expected-proposal.pdf

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

No branches or pull requests

4 participants