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

Parent methods tensor vs. tensor_product #30373

Open
mkoeppe opened this issue Aug 16, 2020 · 92 comments
Open

Parent methods tensor vs. tensor_product #30373

mkoeppe opened this issue Aug 16, 2020 · 92 comments

Comments

@mkoeppe
Copy link
Member

mkoeppe commented Aug 16, 2020

We unify the use of the methods tensor vs. tensor_product in parent classes.

Current situation:

Category ModulesWithBasis provides a parent method tensor to construct a tensor product of modules.

sage: C = CombinatorialFreeModule(QQ, ['x', 'y'])                                                                                                                
sage: C.tensor(C)                                                                                                                                                
Free module generated by {'x', 'y'} over Rational Field # Free module generated by {'x', 'y'} over Rational Field

It is not implemented completely for FreeModule

sage: V = FreeModule(QQ, 2)                                                                                                                                      
sage: V.tensor(V)                                                                                                                                                
AttributeError: type object 'FreeModule_ambient_field_with_category' has no attribute 'Tensor'
sage: 

In contrast, FiniteRankFreeModule (which is not in ModulesWithBasis) uses a parent method tensor to construct elements.

sage: F = FiniteRankFreeModule(QQ, 2)
sage: F.tensor((2, 2))                                                                                                                                           
Type-(2,2) tensor on the 2-dimensional vector space over the Rational Field

FilteredVectorSpace has both tensor (from ModulesWithBasis) and tensor_product, but only the latter works.

sage: FV = FilteredVectorSpace(2)                                                                                                                                
sage: FV.tensor_product(FV)                                                                                                                                      
QQ^4
sage: FV.tensor(FV)                                                                                                                                              
AttributeError: type object 'FilteredVectorSpace_class_with_category' has no attribute 'Tensor'

The same is true for FreeQuadraticModule_integer_symmetric:

sage: L = IntegralLattice(Matrix(ZZ, 2, [2,1,1,-2]))                                                                                                             
sage: L.tensor_product(L)                                                                                                                                        
Lattice of degree 4 and rank 4 over Integer Ring
Standard basis 
Inner product matrix:
[ 4  2  2  1]
[ 2 -4  1 -2]
[ 2  1 -4 -2]
[ 1 -2 -2  4]
sage: L.tensor(L)                                                                                                                                                
AttributeError: type object 'FreeQuadraticModule_integer_symmetric_with_categor' has no attribute 'Tensor'

The proposed solution in this ticket is to standardize on the method tensor_product, making tensor a deprecated alias.

Modules already has a tensor_square method, which tensor_product complements well.

We also add tensor_power.

No changes are made to the global tensor (unique instance of TensorProductFunctor).

Tickets:

See also: #18349

CC: @tscrim @egourgoulhon @mjungmath @jhpalmieri @fchapoton

Component: linear algebra

Author: Matthias Koeppe

Branch/Commit: u/mkoeppe/parent_methods_tensor_vs__tensor_product @ ed6a13f

Issue created by migration from https://trac.sagemath.org/ticket/30373

@mkoeppe mkoeppe added this to the sage-9.2 milestone Aug 16, 2020
@mkoeppe

This comment has been minimized.

@mkoeppe

This comment has been minimized.

@mkoeppe mkoeppe modified the milestones: sage-9.2, sage-9.3 Sep 5, 2020
@mkoeppe

This comment has been minimized.

@jhpalmieri
Copy link
Member

comment:6

A few other things to think about:

  • In the first example, C.tensor(C, C) works as expected. In contrast, the tensor function doesn't take multiple inputs, but instead an iterable: tensor([C,C,C]) instead of tensor(C, C, C). I would prefer a consistent syntax.
  • What about tensoring elements together? Right now the tensor function works on elements, although the documentation doesn't make this clear. I am happy for this to continue, but again, the syntax is different for tensor([a,b,c]) vs. a.tensor(b,c).

@mjungmath
Copy link

comment:7

Replying to @jhpalmieri:

A few other things to think about:

  • In the first example, C.tensor(C, C) works as expected. In contrast, the tensor function doesn't take multiple inputs, but instead an iterable: tensor([C,C,C]) instead of tensor(C, C, C). I would prefer a consistent syntax.

+1. I don't have a strong opinion which one we choose. Multiple inputs feels more flexible (and doesn't need an additional bracket).

  • What about tensoring elements together? Right now the tensor function works on elements, although the documentation doesn't make this clear. I am happy for this to continue, but again, the syntax is different for tensor([a,b,c]) vs. a.tensor(b,c).

I like that behavior, too. However, it feels to me like this behavior was not intended and just works because the element provides a tensor method (that's probably why it's not mentioned in the documentation). From a mathematical-categorial viewpoint this behavior is anyway unexpected (functors usually do not act on objects of objects).

If we want to promote this, which I would agree with, the syntax should indeed be the same.

This might also be a good opportunity to introduce the @ notation you have proposed in here which I really like (so +1 from my side, too).

As for FiniteRankFreeModule, introducing methods like tensor_product and tensor_power is readily done (I already finished the former).

So, should we split the task into several pieces, and I provide the FiniteRankFreeModule add-on? You just have to tell me which syntax you prefer.

@mkoeppe
Copy link
Member Author

mkoeppe commented Jan 21, 2021

comment:8

Replying to @mjungmath:

This might also be a good opportunity to introduce the @ notation you have proposed in here which I really like (so +1 from my side, too).

-1 on this - as per discussion in #30244, @ should be for tensor contraction, not tensor product

@mjungmath
Copy link

comment:9

Replying to @mkoeppe:

Replying to @mjungmath:

This might also be a good opportunity to introduce the @ notation you have proposed in here which I really like (so +1 from my side, too).

-1 on this - as per discussion in #30244, @ should be for tensor contraction, not tensor product

But the idea in principle remains, doesn't it? What about # instead? That's already the symbol for tensor products as in categories/tensor.py.

Arrrg, that's commenting...

@mkoeppe
Copy link
Member Author

mkoeppe commented Jan 21, 2021

comment:10

Python has only a fixed set of binary operators available. One can resort to overloading something that's rarely used otherwise. In #30244 I suggested & as a possibility, but I am not sure if it's a good idea.

@mjungmath
Copy link

comment:11

Replying to @mkoeppe:

Python has only a fixed set of binary operators available. One can resort to overloading something that's rarely used otherwise. In #30244 I suggested & as a possibility, but I am not sure if it's a good idea.

It feels rather uncanonical. But tensor products seem to be used quite frequently, so if there's no better bet...

What's your opinion on the syntax?

Should I open another ticket to provide tensor_product, tensor_power for FiniteRankFreeModule or just push it into here as open branch?

@jhpalmieri
Copy link
Member

comment:12

We should make people use the unicode for tensor product.

More seriously, if we can switch the syntax to tensor(a,b,c,...) with no extra brackets, that would be nice. I don't know if any of the tensor constructors take optional arguments, so I don't know how hard that would be to implement.

@mjungmath
Copy link

comment:13

I've opened a ticket for the FiniteRankFreeModule case: #31276.

@tscrim
Copy link
Collaborator

tscrim commented Jan 22, 2021

comment:14

Multiple inputs can also be really annoying when you want to create a list dynamically as you then have to add a * and possibly extra parentheses. The easiest thing to do is to support both behaviors. This is simple enough to implement.

The tensor unicode would be good, except it would depend on how the Python interpreter takes that. Most likely, we would need to implement another find-replace case in the preparser to go to some overloaded infix operator such as &.

@mjungmath
Copy link

comment:15

Speaking of shortcuts for operations: what about using ^ for the wedge product? There is nothing more appropriate than that!

If that meets your approval, I'll open a ticket for this.

@jhpalmieri
Copy link
Member

comment:16

I find it too easily confused with the exponentiation operator, so I would not like to see it featured prominently in doctests.

@mjungmath
Copy link

comment:17

So far, the ^ is not supported for exterior algebras:

sage: M = FiniteRankFreeModule(QQ, 3)
sage: AM = M.dual_exterior_power(2)
sage: a = AM.an_element()
sage: a^2
Traceback (most recent call last)
...
TypeError: unsupported operand parent(s) for ^: '2nd exterior power of the dual of the 3-dimensional vector space over the Rational Field' and 'Integer Ring'

But I agree. One would rather expect a^3 == a.wedge(a).wedge(a) if supported.

Mh, one minute ago, I thought it was an ingenious idea. :D

@tscrim
Copy link
Collaborator

tscrim commented Jan 23, 2021

comment:18

Replying to @jhpalmieri:

I find it too easily confused with the exponentiation operator, so I would not like to see it featured prominently in doctests.

However, I think it would be a nice syntactic sugar since a ^ b, for a,b in an exterior algebra, as exponentiation does not make sense.

@mjungmath
Copy link

comment:19

Replying to @tscrim:

Replying to @jhpalmieri:

I find it too easily confused with the exponentiation operator, so I would not like to see it featured prominently in doctests.

However, I think it would be a nice syntactic sugar since a ^ b, for a,b in an exterior algebra, as exponentiation does not make sense.

How do we treat ^ with integers then? As a wedge product, this is the simple multiplication. And I still agree, that this case can easily be confused with the exponential since exponentiation is the standard behavior of Sage anywhere else.

@egourgoulhon
Copy link
Member

comment:20

Replying to @jhpalmieri:

We should make people use the unicode for tensor product.

A relevant ticket here: #30473

@egourgoulhon
Copy link
Member

comment:21

Replying to @mjungmath:

Replying to @tscrim:

However, I think it would be a nice syntactic sugar since a ^ b, for a,b in an exterior algebra, as exponentiation does not make sense.

How do we treat ^ with integers then? As a wedge product, this is the simple multiplication. And I still agree, that this case can easily be confused with the exponential since exponentiation is the standard behavior of Sage anywhere else.

+1 (and IMHO a^b looks too far from a wedge product).

@mkoeppe
Copy link
Member Author

mkoeppe commented Feb 13, 2021

comment:22

Setting new milestone based on a cursory review of ticket status, priority, and last modification date.

@mkoeppe mkoeppe modified the milestones: sage-9.3, sage-9.4 Feb 13, 2021
@mkoeppe mkoeppe modified the milestones: sage-9.4, sage-9.5 Jul 19, 2021
@mkoeppe
Copy link
Member Author

mkoeppe commented Sep 5, 2022

comment:68

Mathematically an object is a tensor product if there exists a construction of it using the tensor functor.

In an implementation, we generally cannot know if something exists. An object is marked as a tensor product when the implementation has marked it as such. Like everything, this is subject to the implementation restrictions.

@mkoeppe
Copy link
Member Author

mkoeppe commented Sep 5, 2022

comment:69

Replying to Travis Scrimshaw:

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

I don’t like this fallback for two reasons:

  1. It runs into the same problem with FilteredVectorSpace.tensor_product I gave in comment:40, it doesn’t know how it was built as a tensor product.

The result doesn't have to know because it does not advertise itself as a tensor product.

That is the problem. If I build an object as a tensor product (via the tensor functor), then it should know that because I wanted it to know that explicitly. It should advertise itself as a tensor product.

Suppose as a user I want to construct it anyway. What should I do then?

The class needs to have its own hook separate from the hook that the functor uses.

No, as a user. Suppose there is no such specific implementation. What should the user do?

@tscrim
Copy link
Collaborator

tscrim commented Sep 7, 2022

comment:70

Replying to Matthias Köppe:

Mathematically an object is a tensor product if there exists a construction of it using the tensor functor.

Then every object is a tensor product since we can always tensor with a 1-dimensional module. If we wanted to talk in that level of generality, there is no category of tensor products, Cartesean products, etc.

In an implementation, we generally cannot know if something exists. An object is marked as a tensor product when the implementation has marked it as such. Like everything, this is subject to the implementation restrictions.

Indeed, the category also contains some implementation requirements. (I would argue the tensor products category is the category with a distinguished tensor product construction, parallel to ModulesWithBasis.) Consequently, the result of the functor is promising certain implementation details with its codomain: such objects have methods via the category’s ParentMethods and provide an implementation of the corresponding @abstract_methods.

More concretely, if I had code that does:

T = tensor([V,W])
for F in T.tensor_factors():
     print(F)

This should work whenever T is constructed because the codomain is Cat.TensorProducts(), where Cat is the meet of the categories of V and W. However, with changing the name, the functor now would work but the code would fail with FilteredVectorSpaces.

@tscrim
Copy link
Collaborator

tscrim commented Sep 7, 2022

comment:71

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

I don’t like this fallback for two reasons:

  1. It runs into the same problem with FilteredVectorSpace.tensor_product I gave in comment:40, it doesn’t know how it was built as a tensor product.

The result doesn't have to know because it does not advertise itself as a tensor product.

That is the problem. If I build an object as a tensor product (via the tensor functor), then it should know that because I wanted it to know that explicitly. It should advertise itself as a tensor product.

Suppose as a user I want to construct it anyway. What should I do then?

The class needs to have its own hook separate from the hook that the functor uses.

No, as a user. Suppose there is no such specific implementation. What should the user do?

My guess is stop wishing for magical coding fairies and implement it. There is no generic way to give a tensor product the structure of a specific type of module. This essentially amounts to saying there is a canonical isomorphism between, e.g., R2 (x) R3 = R6. (Another version: How do you build a matrix from a tensor product of two matrices? There are two main ways to do this that are equivalent as they amount to choosing a different order of the isomorphism.) If a class Foo wants to know that a tensor product of two of itself can again be realized as an object Foo, then it needs to implement that and chose a specific isomorphism.

@mkoeppe
Copy link
Member Author

mkoeppe commented Sep 7, 2022

comment:72

Replying to Travis Scrimshaw:

the result of the functor is promising certain implementation details with its codomain: such objects have methods via the category’s ParentMethods and provide an implementation of the corresponding @abstract_methods.

No, I think this is too strict to be useful.

@mkoeppe
Copy link
Member Author

mkoeppe commented Sep 7, 2022

comment:73

Replying to Travis Scrimshaw:

Replying to Matthias Köppe:

No, as a user. Suppose there is no such specific implementation. What should the user do?

My guess is stop wishing for magical coding fairies and implement it.

No, no, users wouldn't like to be talked to like that.

@mkoeppe
Copy link
Member Author

mkoeppe commented Sep 7, 2022

comment:74

We often want to identify 1-fold tensor products with the base module -- and the base module won't have the tensor_factors method. For example, this is what sage.tensor.modules and sage.manifolds.differentiable do.

@tscrim
Copy link
Collaborator

tscrim commented Sep 7, 2022

comment:75

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

the result of the functor is promising certain implementation details with its codomain: such objects have methods via the category’s ParentMethods and provide an implementation of the corresponding @abstract_methods.

No, I think this is too strict to be useful.

That an object in the codomain of a functor (or any morphism) must support what is required by that codomain (and should be an object such that X in Cat returns True)? I find this to actually be quite useful as I can expect certain properties to be there.

Consider the equivalent scenario: I have a function f: ZZ -> QQ[‘s’] that returned 0 as an integer and all non-zero elements as polynomials. You now have to write special checks for 0 rather than have it behave uniformly.

@mkoeppe
Copy link
Member Author

mkoeppe commented Sep 7, 2022

comment:76

Replying to Travis Scrimshaw:

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

the result of the functor is promising certain implementation details with its codomain: such objects have methods via the category’s ParentMethods and provide an implementation of the corresponding @abstract_methods.

No, I think this is too strict to be useful.

That an object in the codomain of a functor (or any morphism) must support what is required by that codomain (and should be an object such that X in Cat returns True)? I find this to actually be quite useful as I can expect certain properties to be there.

There's always a tradeoff. Identifications such as the ones that I mentioned in comment:74 are also quite useful.

@tscrim
Copy link
Collaborator

tscrim commented Sep 7, 2022

comment:77

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

Replying to Matthias Köppe:

No, as a user. Suppose there is no such specific implementation. What should the user do?

My guess is stop wishing for magical coding fairies and implement it.

No, no, users wouldn't like to be talked to like that.

Of course I wouldn’t necessarily tell them in that way. However, unless somebody actually writes the code, then it doesn’t magically come into existence. Unfortunately, this is not the Dire Straights song “Money for Nothing”; Also Hell is full of 10 year olds who wished for exactly the same thing with the holophonor.

In this case, I believe it is impossible to implement in the level of generality you want because it amounts to making a canonical choice where there is not one.

@mkoeppe
Copy link
Member Author

mkoeppe commented Sep 7, 2022

comment:78

Replying to Travis Scrimshaw:

There is no generic way to give a tensor product the structure of a specific type of module. This essentially amounts to saying there is a canonical isomorphism between, e.g., R2 (x) R3 = R6.

It does not have to be canonical to be useful. And in fact one wouldn't construct that but rather construct a free module whose basis is indexed by the cartesian product of the input index sets.

@tscrim
Copy link
Collaborator

tscrim commented Sep 7, 2022

comment:79

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

the result of the functor is promising certain implementation details with its codomain: such objects have methods via the category’s ParentMethods and provide an implementation of the corresponding @abstract_methods.

No, I think this is too strict to be useful.

That an object in the codomain of a functor (or any morphism) must support what is required by that codomain (and should be an object such that X in Cat returns True)? I find this to actually be quite useful as I can expect certain properties to be there.

There's always a tradeoff. Identifications such as the ones that I mentioned in comment:74 are also quite useful.

I agree that they are useful, but you need a canonical way to do it in general. The point is that you are making an identification based upon a specific choice of isomorphism. Individual classes are allowed to make that choice, which you are implicitly doing when you have either a as_foo() method or Foo(T) in your code.

@tscrim
Copy link
Collaborator

tscrim commented Sep 7, 2022

comment:80

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

There is no generic way to give a tensor product the structure of a specific type of module. This essentially amounts to saying there is a canonical isomorphism between, e.g., R2 (x) R3 = R6.

It does not have to be canonical to be useful. And in fact one wouldn't construct that but rather construct a free module whose basis is indexed by the cartesian product of the input index sets.

But that is doing what is mandated by the category. It is holding onto the knowledge of how it is constructed. What you are asking for is something to return the equivalent of R6.

@mkoeppe
Copy link
Member Author

mkoeppe commented Sep 7, 2022

comment:81

I think in comment:79 you replied to the wrong comment.

@tscrim
Copy link
Collaborator

tscrim commented Sep 7, 2022

comment:82

Replying to Matthias Köppe:

I think in comment:79 you replied to the wrong comment.

It applies to both comment:76 and comment:74. I just chose the more recent one.

@mkoeppe
Copy link
Member Author

mkoeppe commented Sep 7, 2022

comment:83

Replying to Travis Scrimshaw:

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

There is no generic way to give a tensor product the structure of a specific type of module. This essentially amounts to saying there is a canonical isomorphism between, e.g., R2 (x) R3 = R6.

It does not have to be canonical to be useful. And in fact one wouldn't construct that but rather construct a free module whose basis is indexed by the cartesian product of the input index sets.

But that is doing what is mandated by the category. It is holding onto the knowledge of how it is constructed. What you are asking for is something to return the equivalent of R6.

No, I don't think this resembles anything what I'm asking for. I think we've lost the context somewhere on the way here.

@mkoeppe
Copy link
Member Author

mkoeppe commented Sep 7, 2022

comment:84

Replying to Travis Scrimshaw:

Replying to Matthias Köppe:

I think in comment:79 you replied to the wrong comment.

It applies to both comment:76 and comment:74. I just chose the more recent one.

Wait, you are concerned about the identification of a module M with its 1-fold tensor product not being canonical?

@mkoeppe
Copy link
Member Author

mkoeppe commented Sep 7, 2022

comment:85

For the question of 1-fold tensor products, there's by the way an old ticket: #18349

@tscrim
Copy link
Collaborator

tscrim commented Sep 7, 2022

comment:86

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

There is no generic way to give a tensor product the structure of a specific type of module. This essentially amounts to saying there is a canonical isomorphism between, e.g., R2 (x) R3 = R6.

It does not have to be canonical to be useful. And in fact one wouldn't construct that but rather construct a free module whose basis is indexed by the cartesian product of the input index sets.

But that is doing what is mandated by the category. It is holding onto the knowledge of how it is constructed. What you are asking for is something to return the equivalent of R6.

No, I don't think this resembles anything what I'm asking for. I think we've lost the context somewhere on the way here.

Sorry, that was a bad phrasing on my part. You are asking for returning R6 to be okay, which you have had to make a (non-canonical) choice of a isomorphism that they user has no control over. The tensor product category has a special distinguished choice of construction as a tensor product, analogous to ModulesWithBasis being modules with a distinguished choice of a basis.

@tscrim
Copy link
Collaborator

tscrim commented Sep 7, 2022

comment:87

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

Replying to Matthias Köppe:

I think in comment:79 you replied to the wrong comment.

It applies to both comment:76 and comment:74. I just chose the more recent one.

Wait, you are concerned about the identification of a module M with its 1-fold tensor product not being canonical?

Not specifically for the 1-fold tensor products, but more generally. This code is expected to work for arbitrary tensor products with no special cases, right? (I can also tell you about the troubles I’ve had with PartitionTuples of length 1 versus Partitions, which motivates my desire for uniformity.)

@mkoeppe
Copy link
Member Author

mkoeppe commented Sep 7, 2022

comment:88

Replying to Travis Scrimshaw:

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

There is no generic way to give a tensor product the structure of a specific type of module. This essentially amounts to saying there is a canonical isomorphism between, e.g., R2 (x) R3 = R6.

It does not have to be canonical to be useful. And in fact one wouldn't construct that but rather construct a free module whose basis is indexed by the cartesian product of the input index sets.

But that is doing what is mandated by the category. It is holding onto the knowledge of how it is constructed. What you are asking for is something to return the equivalent of R6.

No, I don't think this resembles anything what I'm asking for. I think we've lost the context somewhere on the way here.

Sorry, that was a bad phrasing on my part. You are asking for returning R6 to be okay

No

@mkoeppe
Copy link
Member Author

mkoeppe commented Sep 7, 2022

comment:89

Replying to Travis Scrimshaw:

Unfortunately, this is not the Dire Straights song “Money for Nothing”

I do appreciate the attempt to engage in intergenerational dialogue

@tscrim
Copy link
Collaborator

tscrim commented Sep 7, 2022

comment:90

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

There is no generic way to give a tensor product the structure of a specific type of module. This essentially amounts to saying there is a canonical isomorphism between, e.g., R2 (x) R3 = R6.

It does not have to be canonical to be useful. And in fact one wouldn't construct that but rather construct a free module whose basis is indexed by the cartesian product of the input index sets.

But that is doing what is mandated by the category. It is holding onto the knowledge of how it is constructed. What you are asking for is something to return the equivalent of R6.

No, I don't think this resembles anything what I'm asking for. I think we've lost the context somewhere on the way here.

Sorry, that was a bad phrasing on my part. You are asking for returning R6 to be okay

No

Then if you rename the functor hook to tensor_product, then FilteredVectorSpace's method of that name would have a bug.

@tscrim
Copy link
Collaborator

tscrim commented Sep 7, 2022

comment:91

Replying to Matthias Köppe:

Replying to Travis Scrimshaw:

Unfortunately, this is not the Dire Straights song “Money for Nothing”

I do appreciate the attempt to engage in intergenerational dialogue

Actually, that is music I like. ;) Or should I say I appreciate that there's "No One Like You" that I appreciate to have these discussions with.

@mkoeppe

This comment has been minimized.

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

No branches or pull requests

5 participants