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

spec: generics: permit type parameters on aliases #46477

Open
mdempsky opened this issue Jun 1, 2021 · 67 comments
Open

spec: generics: permit type parameters on aliases #46477

mdempsky opened this issue Jun 1, 2021 · 67 comments
Assignees
Labels
generics Issue is related to generics Proposal Proposal-Accepted
Milestone

Comments

@mdempsky
Copy link
Member

mdempsky commented Jun 1, 2021

The generics proposal says "A type alias may refer to a generic type, but the type alias may not have its own parameters. This restriction exists because it is unclear how to handle a type alias with type parameters that have constraints."

I propose this should be relaxed and type aliases allowed to have their own type parameters. I think there's a clear way to handle type aliases with constrained type parameters: uses of the type alias need to satisfy the constraints, and within the underlying type expression those parameters can be used to instantiate other generic types that they satisfy.

I think it's fine to continue allowing type VectorAlias = Vector as in the proposal, but this should be considered short-hand for type VectorAlias[T any] = Vector[T]. More generally, for generic type B with type parameters [T1 C1, T2 C2, ..., Tn Cn], then type A = B would be the same as type A[T1 C1, T2 C2, ..., Tn Cn] = B[T1, T2, ..., Tn].

In particular, something like this would be an error:

type A[T comparable] int
type B[U any] = A[U]   // ERROR: U does not satisfy comparable
type C B[int]

As justification for this, analogous code in the value domain would give an error:

func F(x int) {}
func G(y interface{}) { F(y) }  // ERROR: cannot use y (type interface{}) as int
func H() { G(42) }

I suspect if TParams is moved from Named to TypeName and type instantiation is similarly changed to start from the TypeName instead of the Type, then this should work okay.

/cc @griesemer @ianlancetaylor @findleyr @bcmills

@mdempsky mdempsky added this to the Go1.18 milestone Jun 1, 2021
@findleyr
Copy link
Contributor

findleyr commented Jun 1, 2021

If this proposal were accepted, would the following code be valid?

type A[T any] int
type B[U comparable] = A[U]

I.e. would it be possible to define an alias which tightens the constraints of the aliased type?

IMO the example in the value domain is more analogous to defining a new named type, which already behaves as expected:

type A[T comparable] int
type B[U any] A[U] // ERROR: U does not satisfy comparable

Aliasing seems more analogous to function value assignment, for which we don't redeclare parameters:

func F(x int) {}
var G = F

I think you're right about how this could be implemented, but I wonder if it is conceptually coherent. Specifically, I wonder about whether we should think of the declaration as parameterizing the type, or as defining a parameterized type, and whether it still makes sense to call the example with additional restrictions above an alias.

I'll also note that as you point out, our decisions with respect to the go/types API have real consequences for how easy it would be to relax this restriction on aliases in the future, so it is good to talk about this now. Thanks for raising this issue!

@mdempsky
Copy link
Member Author

mdempsky commented Jun 1, 2021

I.e. would it be possible to define an alias which tightens the constraints of the aliased type?

Yes. U (type parameter with bound comparable) satisfies the constraint any, so that's a valid type declaration in my mind. But similarly, trying to instantiate B[[]int] would be invalid, because []int does not satisfy comparable, even though it satisfies the underlying any.

I would expect that the type checker would see B[[]int], resolve B to the TypeName and check it against the type parameters, and then reject it as invalid, before proceeding to instantiating/substituting its Type with the type argument []int.

Aliasing seems more analogous to function value assignment, for which we don't redeclare parameters:

Note that var G = F is really shorthand there for var G func(int) = F. You're not allowed to write var G func(interface{}) = F, for example, even if you only ever call G with int arguments.

But this is also why I suggest still allowing type A = B as shorthand for explicitly writing out type parameters for the alias declaration.

@griesemer
Copy link
Contributor

griesemer commented Jun 1, 2021

There is a reason why we didn't do this in the first place.

I don't have any principal objections to this proposal. If we accept this, I wonder whether we should still permit the type A = B form as it does deviate from the current design which requires that every use of a generic type requires an instantiation.

I'm inclined to proceed in one of two ways:
1) Disallow (not implement) the form type A = B for Go1.17. It's not crucial and we can always add it later.
2) Implement this proposal instead of permitting type A = B.

@bcmills
Copy link
Contributor

bcmills commented Jun 1, 2021

I seem to recall @rogpeppe raising a similar point in various conversations.

@bcmills
Copy link
Contributor

bcmills commented Jun 1, 2021

@findleyr

Aliasing seems more analogous to function value assignment, for which we don't redeclare parameters:

Note that we do allow function value assignment to strengthen (but not weaken) a type via assignability, which IMO is analogous to strengthening type constraints on a type declaration.

Consider this program:

package main

import "context"

func cancel() {}

type thunk func()

var f = cancel
var g context.CancelFunc = cancel

In that program, var f = cancel is shorthand for var f func() = cancel.

The declaration var g context.CancelFunc = cancel refers to the exact same function value, but with a stronger type (one that is not assignable to thunk).

@jimmyfrasche
Copy link
Member

It looks like it could fall out of the definition but, to be explicit, partial application would also be useful:

type Named[T any] = Map[string, T]

@mdempsky
Copy link
Member Author

mdempsky commented Jun 1, 2021

@griesemer If we proceed with this proposal, I think it could be a nice convenience to keep type A = B as short-hand. But as it's not essential, I'd similarly be fine with just removing it altogether. We can always re-add it in the future if appropriate.

And yes, the deviating from the norm of requiring instantiation is what threw me off. I had written some code that was working under the assumption that if I only started from non-generic declarations, then I would never see a non-instantiated type. But that doesn't hold for the type A = B form. (Fortunately though, it's not hard to special case this one instance either.)

@findleyr
Copy link
Contributor

findleyr commented Jun 1, 2021

@bcmills

Note that we do allow function value assignment to strengthen (but not weaken) a type via assignability, which IMO is analogous to strengthening type constraints on a type declaration.

The example from #46477 (comment) made the analogy of function parameters with type parameters (which makes sense). In that analogy, we don't allow changing function parameter types when assigning [example], i.e. we don't support covariant function assignment.

@bcmills
Copy link
Contributor

bcmills commented Jun 1, 2021

In that analogy … we don't support covariant function assignment.

Sure, but pretty much the entire point of type parameters is to support variance in types. 😉

@neild
Copy link
Contributor

neild commented Jun 2, 2021

What is the use case for permitting parameters on type aliases?

@griesemer
Copy link
Contributor

griesemer commented Jun 2, 2021

@neild The same reason for which type aliases were introduced in the first place, which is to make refactoring across package boundaries easier (or possible, depending on use case).

I misread this comment. See below.

@griesemer
Copy link
Contributor

griesemer commented Jun 2, 2021

Going through my notes I remember now why we didn't go this route in the first place: Note that an alias is just an alternative name for a type, it's not a new type. Introducing a smaller set of type arguments (as suggested above), or providing stronger type constraints seems counter that idea. Such changes arguably define a new type and then one should do that: declare a new defined type, i.e., leave the = away. I note that @findleyr pointed out just that in the 2nd comment on this proposal.
This would mean that the respective methods also have to be redefined (likely as forwarders) but that seems sensible if the type constraints are narrowed or partially instantiated.

In summary, I am not convinced anymore that this is such a good idea. We have explored the generics design space for the greater part of two years and the devil really is in the details. At this point we should not introduce new mechanisms until we have collected some concrete experience.

I suggest we put this on hold for the time being.

@neild
Copy link
Contributor

neild commented Jun 2, 2021

@griesemer I don't see what the refactoring case is for changing the constraints of a type. As you say, an alias is just an alternative name for a type, but an alternative name with altered constraints is a subtler concept that I struggle to see the use for.

I may be missing something. A concrete example of when you'd use this would be useful.

@griesemer
Copy link
Contributor

griesemer commented Jun 2, 2021

@neild Agreed - I misread your comment as "what is the use of allowing alias types for generic types" - my bad. See my comment just before your reply.

@mdempsky
Copy link
Member Author

mdempsky commented Jun 2, 2021

I don't see what the refactoring case is for changing the constraints of a type.

Under this proposal, you can do more with parameterized type aliases than just change the constraints. E.g., see #46477 (comment) for using type parameters to provide default arguments to other generic types. I called out the constraint change to clarify the semantics, not because I expect that's something people are likely to do in practice.

I anticipate analogous to how we added type aliases to facilitate large-scale refactorings while maintaining type identity, we're going to face situations where generic types need to be refactored to add, remove, or change parameters while also maintaining type identity. Having parameterized type aliases would facilitate that. I think if just "declaring a new defined type" was always an adequate solution, we could have skipped adding type aliases too.

I think it's fine though if Go 1.18 doesn't have parameterized type aliases. But I at least think we should try to ensure the go/types APIs are forward compatible with adding parameterized type aliases.

@findleyr
Copy link
Contributor

findleyr commented Jun 2, 2021

@bcmills

Sure, but pretty much the entire point of type parameters is to support variance in types. 😉

FWIW, I don't follow this argument. We still support variance in types no matter what we decide about this proposal, just like we allow variance in function arguments whether or not we allow covariant assignment of function values. I think we're dipping in and out of the 'meta' realm. The point I was trying to make is that if we're trying to argue by analogy with the value domain, wrapping a function is more like defining a new named type (or perhaps more correctly like struct embedding), and aliasing is more like assignment. Since we don't allow covariant assignment for functions, it's arguably a bit inconsistent to allow covariant assignment for "meta functions" (if that's how we think about generic declarations).

@griesemer

This would mean that the respective methods also have to be redefined (likely as forwarders) but that seems sensible if the type constraints are narrowed or partially instantiated.

Or use embedding, which might be more analogous to wrapping a function in the value domain.

I suggest we put this on hold for the time being.

Independent of whether we relax the restriction on aliases, this proposal indirectly makes the point that it matters whether we think of the "type" as generic or the "declaration" as generic, both in the current APIs and for future extensions of the language. For example, thinking of the type declaration as generic allows relaxing this restriction on aliases. Thinking of the function type as generic allows for generic interface methods and generic function literals. If we put this proposal on hold, we will still need to make API decisions that affect its feasibility.

@mdempsky
Copy link
Member Author

mdempsky commented Jun 2, 2021

[re: value vs type domain analogies]

I want to clarify that I made this analogy initially to help explain how I intuit the relationships here. Go's values and types operate sufficiently distinctly and irregularly that I think trying to read too far into the analogy is going to hit rough edges and become more philosophical than actionable. E.g., the value domain has no analog to defined types and type identity, because it's impossible to create a copy of a Go value that's distinguishable from the original. (Emphasis: I'm talking specifically about values here, not variables.)

Certainly we should revisit these discussions when it comes time to add dependent types to Go 3 though. :)

@ianlancetaylor ianlancetaylor added the generics Issue is related to generics label Jun 8, 2021
@rogpeppe
Copy link
Contributor

Note that an alias is just an alternative name for a type, it's not a new type.

I'm not sure that this is entirely true. What about this, which is currently allowed?

type S1[V any] struct { .... }

type S2 = S1[int]

S2 neither an alternative name for an existing type nor an entirely new type. More of a composite type, perhaps. Also, it does have some identity of its own (its name is used when it's embedded)

Introducing a smaller set of type arguments (as suggested above), or providing stronger type constraints seems counter that idea. Such changes arguably define a new type and then one should do that: declare a new defined type, i.e., leave the = away

Sometimes defining a new type isn't possible. For example, if a type is specifically mentioned in a type signature, it's not possible to use a new type - you have to use the same type as the original. Also, the fact that all methods are lost when you define a new type is a real problem and embedding doesn't always work either.

For non-generic code, it might usually be possible to define a fully-qualified type alias like S2 above, but in generic code that's often not possible because a type parameter might be free.

An example:

Say some package defines an OrderedMap container that allows an arbitrary comparison operation for keys:

package orderedmap

type Map[K any, V any, Cmp Comparer[K]] struct {
    ...
}

func (m *Map[K, V, Cmp]) Clone() *Map[K, V, Cmp]

func (m *Map[K, V, Cmp]) Get(k K) (V, bool)

type Comparer[K any] interface {
    Cmp(k1, k2 K) int
}

I want to implement a higher level container in terms of orderedmap.Map. In my implementation, only the value type is generic:

package foo

type Container[V any] struct {
}

func NewContainer[V any]() *Container[V] {
    ...
   var m *orderedmap.Map[internalKey, V, keyComparer]
}

type internalKey struct {
    ...
}

type keyComparer struct{}

func (keyComparer) Cmp(k1, k2 internalKey) int {
    ...
}

In the above code, whenever I wish to pass around the orderedmap.Map[internalKey, V, keyComparer] type, I have to do so explicitly in full. This could end up very tedious (and annoying to change when refactoring the code). It would be nice to be able to do:

type internalMap[V any] = orderedmap.Map[internalKey, V, keyComparer]

Then we can avoid duplicating the type parameters everywhere.

Defining a new type wouldn't be great here - you'd either have to explicitly forward all the methods (if you did type internalMap[V any] orderedmap.Map[...]) or reimplement some of the methods (if you did type internalMap[V any] struct {orderedmap.Map[...]}).

In short, I'm fairly sure that generic type aliases are going to be a much requested feature when people start using generics in seriousness, and that they're definitely worth considering now even if they're not implemented, so that the type checker isn't implemented in a way that makes it hard to add them later.

@rsc
Copy link
Contributor

rsc commented Aug 18, 2021

This proposal has been added to the active column of the proposals project
and will now be reviewed at the weekly proposal review meetings.
— rsc for the proposal review group

@rsc rsc moved this from Incoming to Active in Proposals (old) Aug 18, 2021
@beoran
Copy link

beoran commented Aug 18, 2021

I think we have to consider what exactly an alias is in Go language:

Alias declarations
An alias declaration binds an identifier to the given type.
AliasDecl = identifier "=" Type .
Within the scope of the identifier, it serves as an alias for the type.

Since an alias is an identifier, that is, in essence, a name, it does not make sense for such a name to have type parameters. An alias is simply a name for a type, not a type in itself. And names should not be parameterizable.

If it is desirable to define a new generic type based on an other generic type, this should be done using different syntax than a type alias.

Therefore I, respectfully oppose this proposal.

@mdempsky
Copy link
Member Author

I think we have to consider what exactly an alias is in Go language:

We're discussing amending the Go language spec here, so I think referring to the current wording is somewhat begging the question. If we decide to amend the spec to allow type-parameterized aliases, then I think it's within scope to amend those sentences too.

If it is desirable to define a new generic type based on an other generic type, this should be done using different syntax than a type alias.

We have existing syntax for type aliases and for type parameters, and conveniently they're compatible. I don't see why we'd want to use a new syntax for type-parameterized aliases.

@beoran
Copy link

beoran commented Aug 19, 2021

Well, the reason I refer to the current spec is because that explains what the current concept of am alias is. If we were to change the spec, the concept of what an alias is will also change quite radically. An alias will not be just a name for a type anymore. And I feel this will make Go quite a bit harder to learn.

Furthermore, I would say that the changing concept of an alias as a name to something else is at least not conceptually backwards compatible. With this proposal an alias is not just a name any more, but also a way to define types. Since the concept of this proposal is different, i feel the syntax should be different as well. Maybe just using := in stead of = for example.

@jimmyfrasche
Copy link
Member

This would not let you define any new types: it would let you give a name to a subfamily of a family of types.

@rsc
Copy link
Contributor

rsc commented Aug 25, 2021

What is the concrete benefit that this would bring?
And is it necessary to have in Go 1.18, or should we wait until a future release?

@adonovan
Copy link
Member

One case to consider: would this be legal or not?

type M[K comparable, V any] struct{ ... }
type A[K comparable, V any] = M[K, V]
func (A) Get(K) V { ... }

The spec says "the method receivers must declare the same number of type parameters as present in the generic type definition".

@mdempsky
Copy link
Member Author

@alandonovan I think you mean func (A[K, V]) Get...?

I'd lean towards not allowing that. I argued for allowing type aliases in receiver parameters originally, but now I mostly regret that from an implementation complexity point of view

@griesemer
Copy link
Contributor

griesemer commented Sep 15, 2023

@adonovan I've been thinking about that, too. I'm not sure yet what the right approach is. Consider situations where the alias type has fewer type parameters then the aliased type. And what it we allow more type parameters in the alias than in the aliased type. There are lots of open questions here.

@gopherbot
Copy link

Change https://go.dev/cl/521956 mentions this issue: go/types, types2: introduce _Alias type node

@findleyr
Copy link
Contributor

We've been talking about this on the tools team, and think it would be good to land the support for Alias nodes in go/types one release before supporting parameterized aliases. This will give tools time to opt-in and handle the new node type (a potentially large change), before also supporting parameterized aliases (a likely smaller change). So we could land the new go/types node in 1.22, and parameterized aliases in 1.23.

@DeedleFake
Copy link

Any chance of it going into 1.22 behind a GOEXPERIMENT or a GODEBUG?

@griesemer
Copy link
Contributor

This will be part of 1.23. We won't get this done for 1.22.

gopherbot pushed a commit that referenced this issue Nov 9, 2023
This change introduces a new (unexported for now) _Alias type node
which serves as an explicit representation for alias types in type
alias declarations:

        type A = T

The _Alias node stands for the type A in this declaration, with
the _Alias' actual type pointing to (the type node for) T.
Without the _Alias node, (the object for) A points directly to T.

Explicit _Alias nodes permit better error messages (they mention
A instead of T if the type in the source was named A) and can help
with certain type cycle problems. They are also needed to hold
type parameters for alias types, eventually.

Because an _Alias node is simply an alternative representation for
an aliased type, code that makes type-specific choices must always
look at the actual (unaliased) type denoted by a type alias.
The new function

        func _Unalias(t Type) Type

performs the necessary indirection. Type switches that consider
type nodes, must either switch on _Unalias(typ) or handle the
_Alias case.

To avoid breaking clients, _Alias nodes must be enabled explicitly,
through the new Config flag _EnableAlias.

To run tests with the _EnableAlias set, use the new -alias flag as
in "go test -run short -alias". The testing harness understands
the flag as well and it may be used to enable/disable _Alias nodes
on a per-file basis, with a comment ("// -alias" or // -alias=false)
on the first line in those files. The file-based flag overrides the
command-line flag.

The use of _Alias nodes is disabled by default and must be enabled
by setting _EnableAlias.

Passes type checker tests with and without -alias flag set.

For #25838.
For #44410.
For #46477.

Change-Id: I78e178a1aef4d7f325088c0c6cbae4cfb1e5fb5c
Reviewed-on: https://go-review.googlesource.com/c/go/+/521956
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
Auto-Submit: Robert Griesemer <gri@google.com>
Reviewed-by: Robert Griesemer <gri@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Robert Griesemer <gri@google.com>
@Merovius
Copy link
Contributor

Is this still planned for Go 1.23? It seems now that Go 1.22 is out, having it available for experimentation early in the cycle would be good.

@griesemer
Copy link
Contributor

Yes it is. Some initial work has been done or is in progress. Some of us will be on vacation, too. So it may take some time before anything will be available. Thanks.

AnnaShaleva added a commit to nspcc-dev/dbft that referenced this issue Feb 13, 2024
A part of #2. Use generics instead of util.Uint160 and util.Uint256
types for DBFT and related components. Keep util.Uint160 and util.Uint256
only for specific DBFT implementation in testing code.

The following regressions/behaviour changes were made to properly
apply generics:
1. `dbft.Option` alias is removed since type parameters can't be defined
   on aliases (generic type aliases are prohibited). Ref.
   golang/go#46477.
2. Default dBFT configuration is reduced: all payload-specific defaults
   are removed, as described in #91.
   It is done because default dBFT configuration should not depend on any
   implementation-specific hash type.
3. DBFT configuration validation check is extended wrt point 2.
4. The check if generic `dbft.DBFT` type implements generic `dbft.Service`
   interface is removed since such check should be performed on particular
   (non-generic) DBFT implementation.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
AnnaShaleva added a commit to nspcc-dev/dbft that referenced this issue Feb 13, 2024
A part of #2. Use generics instead of util.Uint160 and util.Uint256
types for DBFT and related components. Keep util.Uint160 and util.Uint256
only for specific DBFT implementation in testing code.

The following regressions/behaviour changes were made to properly
apply generics:
1. `dbft.Option` alias is removed since type parameters can't be defined
   on aliases (generic type aliases are prohibited). Ref.
   golang/go#46477.
2. Default dBFT configuration is reduced: all payload-specific defaults
   are removed, as described in #91.
   It is done because default dBFT configuration should not depend on any
   implementation-specific hash type.
3. DBFT configuration validation check is extended wrt point 2.
4. The check if generic `dbft.DBFT` type implements generic `dbft.Service`
   interface is removed since such check should be performed on particular
   (non-generic) DBFT implementation.
5. `dbft.Service` interface is removed since it's unused.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
AnnaShaleva added a commit to nspcc-dev/dbft that referenced this issue Feb 13, 2024
A part of #2. Use generics instead of util.Uint160 and util.Uint256
types for DBFT and related components. Keep util.Uint160 and util.Uint256
only for specific DBFT implementation in testing code.

The following regressions/behaviour changes were made to properly
apply generics:
1. `dbft.Option` alias is removed since type parameters can't be defined
   on aliases (generic type aliases are prohibited). Ref.
   golang/go#46477.
2. Default dBFT configuration is reduced: all payload-specific defaults
   are removed, as described in #91.
   It is done because default dBFT configuration should not depend on any
   implementation-specific hash type.
3. DBFT configuration validation check is extended wrt point 2.
4. The check if generic `dbft.DBFT` type implements generic `dbft.Service`
   interface is removed since such check should be performed on particular
   (non-generic) DBFT implementation.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
AnnaShaleva added a commit to nspcc-dev/dbft that referenced this issue Feb 13, 2024
A part of #2. Use generics instead of util.Uint160 and util.Uint256
types for DBFT and related components. Keep util.Uint160 and util.Uint256
only for specific DBFT implementation in testing code.

The following regressions/behaviour changes were made to properly
apply generics:
1. `dbft.Option` alias is removed since type parameters can't be defined
   on aliases (generic type aliases are prohibited). Ref.
   golang/go#46477.
2. Default dBFT configuration is reduced: all payload-specific defaults
   are removed, as described in #91.
   It is done because default dBFT configuration should not depend on any
   implementation-specific hash type.
3. DBFT configuration validation check is extended wrt point 2.
4. The check if generic `dbft.DBFT` type implements generic `dbft.Service`
   interface is removed since such check should be performed on particular
   (non-generic) DBFT implementation.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
@drnkwati
Copy link

drnkwati commented Feb 19, 2024

Can we expect generic type parameter aliasing in Go 1.23 release?
I wish I could alias generic types like:

type Blueprint[K comparable, V any] struct {
	items map[K]V
}
type Dictionary[K comparable, V any] = Blueprint[K, V]

golang generic type cannot be alias

@gopherbot
Copy link

Change https://go.dev/cl/566856 mentions this issue: go/types, types2: initial support for parameterized type aliases

gopherbot pushed a commit that referenced this issue Feb 28, 2024
Permit type parameters on type alias declarations depending on
Go language version.

Implement various version checks such that at most one version
error is reported per type alias declaration.

Add tparams field to Alias type node.

Missing:
        - instantiation of alias types
        - API additions (requires proposal)

For #46477.

Change-Id: Ica658292bd096d3bceb513027d3353501a6c58e4
Reviewed-on: https://go-review.googlesource.com/c/go/+/566856
Auto-Submit: Robert Griesemer <gri@google.com>
Reviewed-by: Robert Griesemer <gri@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
@gopherbot
Copy link

Change https://go.dev/cl/567617 mentions this issue: types2: steps towards instantiation of generic alias types

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
generics Issue is related to generics Proposal Proposal-Accepted
Projects
Status: Accepted
Development

No branches or pull requests