Open kortschak opened 3 years ago
Note that complex
and real
are not keywords; they are predeclared identifiers. This proposal effectively extends complex
and real
to be ordinary (generic) functions when invoked on a value, but to be meta-functions from types to types when invoked on a type. We have many operators that can be used to convert one type to another type (e.g., the *
operator converts T
to pointer-to-T
), but there would be the first functions. I don't think that is a blocker for this proposal, I'm just pointing it out.
CC @griesemer
Thanks, @ianlancetaylor. Yes, that was something I was considering in terms of backwards compatibility. I was trying to see if there was something that the syntax choice here was cause problems with but was unable to find any (of course they may exist). Other non-function based syntax is obviously also possible.
I quite like the view of complex types as generic types.
Suppose that we'd already had generics in the language and wanted to add support for complex values. We could implement much of the current spec with something like this (defined in global scope):
// not currently in global scope but could be.
type realType interface {
type float32, float64
}
// not currently in global scope but could be.
type complexType[T realType] struct {
real, imag T
}
type complex64 = complexType[float32]
type complex128 = complexType[float64]
func imag[T realType](x complexType[T]) T {
return x.imag
}
func real[T realType](x complexType[T]) T {
return x.real
}
func complex[T realType](r, i T) complexType[T] {
return complexType[T]{r, i}
}
Perhaps if we think of the existing complex types as a kind of sugar over the above, then it might make sense to allow complex[float32]
and complex[float64]
as synonyms for complex64
and complex128
respectively.
One issue with that is that this would make the type complex
synonymous with the constructor function complex
, which is awkward because then there would be an ambiguity between a type conversion complex(x)
and a constructor expression complex(r, i)
. We could disambiguate by treating it as a type conversion when there's one argument and a constructor when there are two, but that feels a little bit dubious.
It also opens up a question: given that type lists allow any type whose underlying types are mentioned, this would also admit complex[floatType]
where floatType
is a defined type with underlying type float32
or float64
.
Would that be OK?
If complex values are represented as structs, these issues do not exist, for example, here. However, this shuts our any use of the language's complex types
FWIW I always felt that the inclusion of complex numbers into the language was a bit strange. IMO, adding more strangeness on top to make them work with generics is bringing the sunk cost fallacy to mind. I think it might well be worthy of consideration to deprecate the builtin complex types in favor of a library-implementation of them - which can actually be built with generics. The generic type could live in math
, for example.
In my opinion, this example might show one of the limits of the current form of the proposal where constraints are derived from types.
The opposite should happen, i.e. types should be derived from constraints. (with a way to extract the original constraints of a type so that the general user can constrain type parameters)
Then we could have functions of constraints object argument that returns constraints (Propositional functions). This is different from using types as constraints because not all constraints are necessarily types. They could be propositions (as in "convertible to X") Propositions and not predicates. I think this ought to be restricted to zeroth-order logic. (application of propositional calculus)
I will reference here again the little warning from "A gentle introduction to semantic subtyping" on page 200 as this seem to remotely apply here. https://www.irif.fr/~gc/papers/icalp-ppdp05.pdf
Their issue is about subtyping relations but I think the more general issue is about type constraint propagation... i.e. whenever there is a relation between types. In the present case for instance or notably when we deal with interfaces, composite types such as structs etc.
The constraint package would eventually just be the partial constraint store of the default constraint system induced into the type system. Then we would have complex type constraints resulting from a propositional function applied to float type constraints. Just to be applied to a type parameter if needs be.
Just a note.
I don't know what the downvote is for since I see no explication but said in some more simpler terms, types should be built from constraint sets.
An interface would be like a regular type with some structural and the nominal constraints lifted. Effectively allowing subtyping (but not parametricity here because interfaces are themselves types) .
This is going to be important for any other composite types and especially if we want typelists are regular interfaces.
Propositional functions would allow to define precisely the constraint that form a complex type depending on the constraints of the argument float type.
Funnily enough, this is the goal of semantic subtyping: allowing parametricity in the presence of structural subtyping. If we define subtyping as partial constraint lifting, it's perhaps easier to understand.
@atdiar I gave a thumbs-down because it sounds to me like you are suggesting a relatively fundamental change to the generics design - not just an amendment to address OPs concerns. I felt that this is neither the time nor place for that discussion. I also didn't want to drag the issue further off-topic, so I tried leaving it with an emoji reaction.
Yes but OP concerns are to me a part of a more general problem with the design. I think it fits here.
It explains the idea of a complex higher-order function that takes a float type as input for instance.
Thank you for the clarification.
@Merovius Regarding the removal of built-in complex types, there are two parts that need to be addressed. The first is likely trivial, it would need a rewrite tool to convert the existing infix notation to function calls (we have an extensive collection of functions that work on complex values and a manual rewrite is a non-starter). The second is a judgement call; reading conventional infix notation is much easier for most people. We have other rings (quaternion, dual, hyperdual, dual quaternion and noncom dual complex) implemented and using those systems is harder than for the complex types. If a rewrite tool for thing 1 existed I guess it could be coopted to make a code generation tool to convert from infix to function call forms though.
Yeah, my comment was originally prompted by this sentence from your mail to golang-nuts:
Indeed, in the current generics proposal, the Gonum quaternion, dual, hyperdual and dual quaternion number types are easier to integrate into a generics system than the built in complex number types because they are based on structs.
I think the same readability concern also transfers to other types, like big.{Int,Float}
or GL(n, R)
. Singling out ℂ seems based on the prior that they are already natively represented in the language - that's where the sunk cost fallacy comes in :)
Personally, it seems more elegant to me to address the readability issue separately, if we are worried about it. With a solution that can be applied to other library-types as well. If Go had operator overloading, that would solve the readability issue of representing ℂ as structs while also helping Quaternions, *big.Int
and GL(n,R)
code, while not requiring us to bolt on further exceptions for complex
to the generics design. If, on the other hand, we think the costs of operator overloading outweigh their benefits, the same calculus that leads us to conclude that for *big.Int
should also be applied to ℂ.
In any case, I just wanted to make the option explicit :) I'm content to just wait and see where the rest of the discussion is going :)
I'll just comment that instead of
type complex64 = complexType[float32]
you could also use
type complex64 = typeof(complex(float32(0), float32(0)))
if we had typeof (and that could apply to much more than complex and float problems).
@rsc can I clarify the proposal status of this issue? It has been removed from Incoming, but not placed in Active. Was this intentional?
It's now labeled as a Go2 proposal.
On Wed, Apr 7, 2021, 12:40 PM Dan Kortschak @.***> wrote:
@rsc https://github.com/rsc can I clarify the proposal status of this issue? It has been removed from Incoming, but not placed in Active. Was this intentional?
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/golang/go/issues/45049#issuecomment-815175036, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACBCIT46UXRRWHMOXL3H5IDTHSYJRANCNFSM4ZHUISPQ .
Maybe how that fits in with https://github.com/golang/proposal#readme could be clarified? This otherwise feels kind of arbitrary.
We have in the past liberally moved proposals between "regular" and "Go2" proposals, so I don't think this is arbitrary.
In the Go2 proposal reviews we tend to spend more time on language design issues, sometimes discussing just one or two proposals at length. It seems that this proposal fits better in that process for now. It might move back once we have decided on the best way forward.
So if anything, this relabelling should free up space for a more in-depth discussion.
OK. Thanks for clarifying that. Perhaps some text in the proposal#readme could be added to note this progress path?
I think the same readability concern also transfers to other types, like
big.{Int,Float}
orGL(n, R)
. Singling out ℂ seems based on the prior that they are already natively represented in the language - that's where the sunk cost fallacy comes in :)
Is there interest in extending such a proposal to a broader syntax for destructuring uncommon types in general? Something like the following example:
type Person struct {
Firstname, Lastname string
NumberOfCats,NumberOfDogs int
}
type Name struct {
FName, LName string
}
type Pets struct {
Cats, Dogs int
}
func DestructuringExample(person Person) bool {
name, pets := person.{Name, Pets}
name.FName = "Ted"
return person.Firstname == name.FName
}
func DestructuringExample2(person Person) bool {
name, pets := (*Name)(unsafe.Pointer(&person)), (*Name)(add(unsafe.Pointer(&person),sizeof(Name))
name.FName = "Ted"
return person.Firstname == name.FName
}
Specific concerns would be whether it would be an issue that this exposes otherwise unexported types, and that this now creates a strong coupling between the involved named types. Nonetheless, this type of destructuring can be currently done with raw pointers, though it is considerable more brittle and is not statically checkable.
P.S. I want this. Omigosh, I want this. If it's tangental to the topic of complex number expansion, regardless of whether this proposal is acceptable, I will absolutely write up a proposal.
Rather than using complex
as a general type function, we can restrict it to be only used as a constraint. The expression complex(T)
, where T
is a type that must be a floating-point type, is the complex form of that floating-point type, either complex64
or complex128
.
We don't need anything else, because we can rely on type inference.
type Float interface { ~float32 | ~float64 }
func ToComplex[F Float, C ~complex(F)](f F) C { return complex(f, 0) }
func RealPart[C ~complex(F), F Float](c C) F { return real(c) }
In these examples, the function ordinary argument determines the first type argument, and the second type argument is inferred from the first type argument.
A slightly different syntax would be to use complex[R]
as the constraint. That would make it clearer that we are describing (instantiating) a type rather than calling a function. This also does not introduce new syntax for constraints. I guess this has some similarities to what @rogpeppe suggested above, although it's not clear that we would want to permit this syntax to be used for anything than a constraint.
To follow-up on @ianlancetaylor's comment, another way of stating this is to say that complex types are (special, built-in) composite types of which there exist two types: complex[float32]
and complex[float64]
, very much the same as the (special, built-in) composite map and channel types (of which there happen to be infinitely many types).
And for historic reasons we also have the aliases
type complex64 = complex[float32]
type complex128 = complex[float64]
The reason why these complex[T] types are not defined as structs is because they allow operations (such as the usual arithmetic operations) which are not available on structs.
The one anomaly we'd have is the fact that complex[T]
is both a built-in type and a built-in function, but that may be the price to pay if we want to keep the name complex
. (I suppose one could use something like cmplx[float32]
and avoid this issue as well, which is perhaps ok since it would only appear in constraints, given that we also have the aliases.)
@ianlancetaylor @griesemer variously s/float128/float32/g
and s/float128/float64/g
?
@ianlancetaylor I think probably this
func ToComplex[F Float, C ~complex(F)](f F) C { return complex(f) }
is
func ToComplex[F Float, C ~complex(F)](f F) C { return complex(f, 0) }
Is this correct? If so, I think we could live with this approach.
Yes, your understanding is correct. Sorry for the typos. I think I've fixed them now.
If we were to have:
type complex64 = complex[float32]
type complex128 = complex[float64]
Then could we write these?
type MyFloat float64
type MyComplex complex[MyFloat]
type ConstraintComplex[T float32 | float64] complex[T]
type AnyComplex128[T ~float64] complex[T]
Background
The Type Parameters Generics proposal has been accepted so type-parametric generic functions will be part of the language in the not wildly distant future. Currently there is a gap in the proposal that has been raised in golang-nuts, here, here and here.
The issue in those discussions is that the type parameterisation approach does not currently allow any kind of correlated structured types unless the structured type is a composite type (structs, arrays, slices, and maps); the current approach covers the majority of structured types but misses types that have
complex64
andcomplex128
as their underlying type. Under the current proposal it is not possible to write something like this:Another example in the opposite direction, showing a use that would useful in generic DSP code is to allow the conversion of a real value to the same value represented as a complex number.
where
?
indicates some type specification that would be thefloat
type corresponding to thecomplex
T
.The examples shown here are trivial (though relevant to a generic "math/cmplx" package), but more significant issues exist for example the example given in the first golang-nuts link above, a generic Eigen Decomposition function which takes a real m×n matrix and returns a pair of complex matrices and a complex vector.
If complex values are represented as structs, these issues do not exist, for example, here. However, this shuts out any use of the language's complex types, and it requires that complex types be referred to by their real coefficients' types, rather than by their actual complex type.
Alternatively, the function could be implemented as follows
where the returned slices hold alternating real and imaginary parts, but the 70s called and they want their language back, and this does not address the issue with non-slice types.
Proposal
I propose that the
complex
andreal
keywords be adopted to allow obtaining the correlated type for real and complex values respectively. This would allow the examples above to be written as follows:The choice of
complex
is obvious, thoughreal
is perhaps less so, it can be read as the type of the real coefficients of the complex type, rather than the real part. For this reason,imag(T)
is not appropriate to obtain the float correspondent ofT
. Iffloat
were still part of the language it could have been chosen.Since complex values' parts must match, the complex corresponding to a float
T
, is specified ascomplex(T)
rather thancomplex(T, T)
.A potential alternative syntax instead of
complex(T)
andreal(T)
iscomplex[T]
andreal[T]
, though this would be ambiguous with indexing in the code body.