Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

Post History

84%
+9 −0
Q&A When would one not want to return an interface?

From simple subtyping concerns, you want the arguments of your methods to be as abstract/imprecise as possible while still allowing you to efficiently accomplish your goals. This allows your method...

posted 2y ago by Derek Elkins‭  ·  edited 2y ago by Derek Elkins‭

Answer
#2: Post edited by user avatar Derek Elkins‭ · 2022-12-25T04:33:19Z (almost 2 years ago)
typo
  • From simple subtyping concerns, you want the arguments of your methods to be as abstract/imprecise as possible while still allowing you to efficiently accomplish your goals. This allows your method to be used in the broadest range possible, and it also makes it clear what exactly your method depends upon. Dually, you want the return type of your methods to be as concrete/precise as possible while not provide any guarantees you aren't willing to ensure over time. Again, this allows your method to be used in the broadest range possible. It also communicates which guarantees you are providing.
  • For your example, the only reason to return an `IList<int>` rather than a `List<int>` is if you want to reserve the right to return some other implementation of `IList<int>` in the future. The disadvantage of returning `IList<int>` rather than `List<int>` is it gives less information and fewer guarantees to the consumer. The consumer can't exploit any `List`-specific methods or other functions that consume specifically `List`s and not `IList`s in general. They also lose guarantees in behavior. There are very many possible implementations of `IList` that observably don't behave like a `List`. In practice, I think it is pretty common that there's no realistic expectation that we'd change the return type for a method especially for more data-like objects as opposed to service-like objects.
  • ----
  • <small>I wrote the remainder before I decided to take the tack articulated in the above paragraphs. I think the above paragraphs make the case on their own, so reading the following paragraphs isn't necessary, but I didn't feel like just throwing them away. They are essentially more examples of the importance and value of precise types and the guarantees they provide.</small>
  • ----
  • Structural types like sum and product (i.e. tuple) types tend to have particularly important guarantees and looser interfaces will lead to the loss of our ability to rely on these guarantees. Values of structural types are arguably "generic" data-like objects.
  • Starting with (immutable) tuples and specifically pairs first, the idea of a pair type like `(A, B)` is that its values contain *exactly* a value of type `A` and a value of type `B`. For the purposes of consuming a tuple type, an interface with `getA` and `getB` methods would suffice (ignoring potential side-effects), but such an interface doesn't preclude a `getC`, etc. If we don't ignore side-effects, then the interface isn't even adequate for consumption. The point being that the pair type communicates that *all* of the relevant information is contained in the two components. This is represented by a (weakened^[The unweakened version would have `p`, `a`, and `b` be *expressions* and not just variables, but then these equations wouldn't hold due to side-effects.]) universal property of the form: `(p.getA(), p.getB()) = p`, `(a, b).getA() = a`, and `(a, b).getB() = b` where the equalities are value equalities, though the latter two should be true even at the level of object identity. The first equation really drives home the fact that the components of a pair contain all the information of that pair. When you're transforming these pairs and not just reading out their components, these universal properties matter.
  • The above is why types like `Point2D` representing a point on a plane and data transfer objects (DTOs) and, more generally, value objects are often passed around directly. Before moving on to sums, I'll say that the above does not *preclude* using an interface conceptually. There are differing implementations of pairs, especially for concrete cases like `Point2D` which we may represent as polar or cartesian coordinates. Theoretically, the implementations would nevertheless be isomorphic and thus not differ in any observable behavior, e.g. they may have different performance characteristics but they always behave the same. Unfortunately, C#, like most languages, lacks a way to express, let alone enforce, contracts on interfaces that would guarantee this.
  • Sum types are dual to product types and thus have dual concerns. See below for one possible encoding of a 2-variant sum type called `Either` (following the name Haskell uses). The concerns are arguably much sharper in this case. The universal property in this case (slightly simplifying the notation) is: `new Left(a).Match<C>(a => x, b => y) = x`, `new Right(b).Match<C>(a => x, b => y) = y`, and `e.Match<Either<A, B>>(a => new Left(a), b => new Right(b)) = e`. One of the key benefits of (closed) sum types is that they are exhaustive. Our `Either` type has two cases, `Left` and `Right`, and once we've specified what to do in those two cases, then we've covered all cases. If `Either` was an interface that allowed other implementations, we would not have this guarantee. Further, sum types allow us to decrease precision. Clearly, a type that could be either `A` or `B` is less precise than one that is always `A`. To this end, it can sometimes be beneficial to have a return type like `Either<A, B>.Right` rather than just `Either<A, B>` as that provides more precision. Consumers no longer have to handle a `Left` case that can't happen.
  • In general, concrete types are more precise than interfaces. Most of the time, we don't need or want that level of precision, but sometimes we do, or at least we want more precision than can be captured by C#'s type system without going all the way to a concrete type. Structural types have structural properties that we do tend to care about and are not captured by analogous interfaces. Many languages build these types in the form of tuples, records, "generalized" enumerations, and algebraic data types.
  • ```csharp
  • public abstract class Either<A, B> {
  • private Either() {} // Trick to disallow any other subclasses.
  • public abstract C Match<C>(Func<A, C> left, Func<B, C> right);
  • public sealed class Left : Either<A, B> {
  • public readonly A Value;
  • public Left(A a) { this.Value = a; }
  • public override C Match<C>(Func<A, C> left, Func<B, C> right) {
  • return left(this.Value);
  • }
  • }
  • public sealed class Right : Either<A, B> {
  • public readonly B Value;
  • public Right(B b) { this.Value = b; }
  • public override C Match<C>(Func<A, C> left, Func<B, C> right) {
  • return right(this.Value);
  • }
  • }
  • }
  • ```
  • From simple subtyping concerns, you want the arguments of your methods to be as abstract/imprecise as possible while still allowing you to efficiently accomplish your goals. This allows your method to be used in the broadest range possible, and it also makes it clear what exactly your method depends upon. Dually, you want the return type of your methods to be as concrete/precise as possible while not providing any guarantees you aren't willing to ensure over time. Again, this allows your method to be used in the broadest range possible. It also communicates which guarantees you are providing.
  • For your example, the only reason to return an `IList<int>` rather than a `List<int>` is if you want to reserve the right to return some other implementation of `IList<int>` in the future. The disadvantage of returning `IList<int>` rather than `List<int>` is it gives less information and fewer guarantees to the consumer. The consumer can't exploit any `List`-specific methods or other functions that consume specifically `List`s and not `IList`s in general. They also lose guarantees in behavior. There are very many possible implementations of `IList` that observably don't behave like a `List`. In practice, I think it is pretty common that there's no realistic expectation that we'd change the return type for a method especially for more data-like objects as opposed to service-like objects.
  • ----
  • <small>I wrote the remainder before I decided to take the tack articulated in the above paragraphs. I think the above paragraphs make the case on their own, so reading the following paragraphs isn't necessary, but I didn't feel like just throwing them away. They are essentially more examples of the importance and value of precise types and the guarantees they provide.</small>
  • ----
  • Structural types like sum and product (i.e. tuple) types tend to have particularly important guarantees and looser interfaces will lead to the loss of our ability to rely on these guarantees. Values of structural types are arguably "generic" data-like objects.
  • Starting with (immutable) tuples and specifically pairs first, the idea of a pair type like `(A, B)` is that its values contain *exactly* a value of type `A` and a value of type `B`. For the purposes of consuming a tuple type, an interface with `getA` and `getB` methods would suffice (ignoring potential side-effects), but such an interface doesn't preclude a `getC`, etc. If we don't ignore side-effects, then the interface isn't even adequate for consumption. The point being that the pair type communicates that *all* of the relevant information is contained in the two components. This is represented by a (weakened^[The unweakened version would have `p`, `a`, and `b` be *expressions* and not just variables, but then these equations wouldn't hold due to side-effects.]) universal property of the form: `(p.getA(), p.getB()) = p`, `(a, b).getA() = a`, and `(a, b).getB() = b` where the equalities are value equalities, though the latter two should be true even at the level of object identity. The first equation really drives home the fact that the components of a pair contain all the information of that pair. When you're transforming these pairs and not just reading out their components, these universal properties matter.
  • The above is why types like `Point2D` representing a point on a plane and data transfer objects (DTOs) and, more generally, value objects are often passed around directly. Before moving on to sums, I'll say that the above does not *preclude* using an interface conceptually. There are differing implementations of pairs, especially for concrete cases like `Point2D` which we may represent as polar or cartesian coordinates. Theoretically, the implementations would nevertheless be isomorphic and thus not differ in any observable behavior, e.g. they may have different performance characteristics but they always behave the same. Unfortunately, C#, like most languages, lacks a way to express, let alone enforce, contracts on interfaces that would guarantee this.
  • Sum types are dual to product types and thus have dual concerns. See below for one possible encoding of a 2-variant sum type called `Either` (following the name Haskell uses). The concerns are arguably much sharper in this case. The universal property in this case (slightly simplifying the notation) is: `new Left(a).Match<C>(a => x, b => y) = x`, `new Right(b).Match<C>(a => x, b => y) = y`, and `e.Match<Either<A, B>>(a => new Left(a), b => new Right(b)) = e`. One of the key benefits of (closed) sum types is that they are exhaustive. Our `Either` type has two cases, `Left` and `Right`, and once we've specified what to do in those two cases, then we've covered all cases. If `Either` was an interface that allowed other implementations, we would not have this guarantee. Further, sum types allow us to decrease precision. Clearly, a type that could be either `A` or `B` is less precise than one that is always `A`. To this end, it can sometimes be beneficial to have a return type like `Either<A, B>.Right` rather than just `Either<A, B>` as that provides more precision. Consumers no longer have to handle a `Left` case that can't happen.
  • In general, concrete types are more precise than interfaces. Most of the time, we don't need or want that level of precision, but sometimes we do, or at least we want more precision than can be captured by C#'s type system without going all the way to a concrete type. Structural types have structural properties that we do tend to care about and are not captured by analogous interfaces. Many languages build these types in the form of tuples, records, "generalized" enumerations, and algebraic data types.
  • ```csharp
  • public abstract class Either<A, B> {
  • private Either() {} // Trick to disallow any other subclasses.
  • public abstract C Match<C>(Func<A, C> left, Func<B, C> right);
  • public sealed class Left : Either<A, B> {
  • public readonly A Value;
  • public Left(A a) { this.Value = a; }
  • public override C Match<C>(Func<A, C> left, Func<B, C> right) {
  • return left(this.Value);
  • }
  • }
  • public sealed class Right : Either<A, B> {
  • public readonly B Value;
  • public Right(B b) { this.Value = b; }
  • public override C Match<C>(Func<A, C> left, Func<B, C> right) {
  • return right(this.Value);
  • }
  • }
  • }
  • ```
#1: Initial revision by user avatar Derek Elkins‭ · 2022-12-25T04:31:29Z (almost 2 years ago)
From simple subtyping concerns, you want the arguments of your methods to be as abstract/imprecise as possible while still allowing you to efficiently accomplish your goals. This allows your method to be used in the broadest range possible, and it also makes it clear what exactly your method depends upon. Dually, you want the return type of your methods to be as concrete/precise as possible while not provide any guarantees you aren't willing to ensure over time. Again, this allows your method to be used in the broadest range possible. It also communicates which guarantees you are providing.

For your example, the only reason to return an `IList<int>` rather than a `List<int>` is if you want to reserve the right to return some other implementation of `IList<int>` in the future. The disadvantage of returning `IList<int>` rather than `List<int>` is it gives less information and fewer guarantees to the consumer. The consumer can't exploit any `List`-specific methods or other functions that consume specifically `List`s and not `IList`s in general. They also lose guarantees in behavior. There are very many possible implementations of `IList` that observably don't behave like a `List`. In practice, I think it is pretty common that there's no realistic expectation that we'd change the return type for a method especially for more data-like objects as opposed to service-like objects.

----

<small>I wrote the remainder before I decided to take the tack articulated in the above paragraphs. I think the above paragraphs make the case on their own, so reading the following paragraphs isn't necessary, but I didn't feel like just throwing them away. They are essentially more examples of the importance and value of precise types and the guarantees they provide.</small>

----

Structural types like sum and product (i.e. tuple) types tend to have particularly important guarantees and looser interfaces will lead to the loss of our ability to rely on these guarantees. Values of structural types are arguably "generic" data-like objects.

Starting with (immutable) tuples and specifically pairs first, the idea of a pair type like `(A, B)` is that its values contain *exactly* a value of type `A` and a value of type `B`. For the purposes of consuming a tuple type, an interface with `getA` and `getB` methods would suffice (ignoring potential side-effects), but such an interface doesn't preclude a `getC`, etc. If we don't ignore side-effects, then the interface isn't even adequate for consumption. The point being that the pair type communicates that *all* of the relevant information is contained in the two components. This is represented by a (weakened^[The unweakened version would have `p`, `a`, and `b` be *expressions* and not just variables, but then these equations wouldn't hold due to side-effects.]) universal property of the form: `(p.getA(), p.getB()) = p`, `(a, b).getA() = a`, and `(a, b).getB() = b` where the equalities are value equalities, though the latter two should be true even at the level of object identity. The first equation really drives home the fact that the components of a pair contain all the information of that pair. When you're transforming these pairs and not just reading out their components, these universal properties matter.

The above is why types like `Point2D` representing a point on a plane and data transfer objects (DTOs) and, more generally, value objects are often passed around directly. Before moving on to sums, I'll say that the above does not *preclude* using an interface conceptually. There are differing implementations of pairs, especially for concrete cases like `Point2D` which we may represent as polar or cartesian coordinates. Theoretically, the implementations would nevertheless be isomorphic and thus not differ in any observable behavior, e.g. they may have different performance characteristics but they always behave the same. Unfortunately, C#, like most languages, lacks a way to express, let alone enforce, contracts on interfaces that would guarantee this.

Sum types are dual to product types and thus have dual concerns. See below for one possible encoding of a 2-variant sum type called `Either` (following the name Haskell uses). The concerns are arguably much sharper in this case. The universal property in this case (slightly simplifying the notation) is: `new Left(a).Match<C>(a => x, b => y) = x`, `new Right(b).Match<C>(a => x, b => y) = y`, and `e.Match<Either<A, B>>(a => new Left(a), b => new Right(b)) = e`. One of the key benefits of (closed) sum types is that they are exhaustive. Our `Either` type has two cases, `Left` and `Right`, and once we've specified what to do in those two cases, then we've covered all cases. If `Either` was an interface that allowed other implementations, we would not have this guarantee. Further, sum types allow us to decrease precision. Clearly, a type that could be either `A` or `B` is less precise than one that is always `A`. To this end, it can sometimes be beneficial to have a return type like `Either<A, B>.Right` rather than just `Either<A, B>` as that provides more precision. Consumers no longer have to handle a `Left` case that can't happen.

In general, concrete types are more precise than interfaces. Most of the time, we don't need or want that level of precision, but sometimes we do, or at least we want more precision than can be captured by C#'s type system without going all the way to a concrete type. Structural types have structural properties that we do tend to care about and are not captured by analogous interfaces. Many languages build these types in the form of tuples, records, "generalized" enumerations, and algebraic data types.

```csharp
public abstract class Either<A, B> {
  private Either() {} // Trick to disallow any other subclasses.

  public abstract C Match<C>(Func<A, C> left, Func<B, C> right);

  public sealed class Left : Either<A, B> {
    public readonly A Value;
    public Left(A a) { this.Value = a; }
    public override C Match<C>(Func<A, C> left, Func<B, C> right) {
      return left(this.Value);
    }
  }    

  public sealed class Right : Either<A, B> {
    public readonly B Value;
    public Right(B b) { this.Value = b; }
    public override C Match<C>(Func<A, C> left, Func<B, C> right) {
      return right(this.Value);
    }
  }
}
```