15
$\begingroup$

As per the comment about Swift having an Any type here (and my related question in that post, on how to access type metadata at runtime without increasing the output build bloat), and because of the points made here, I am thinking a statically typed language is best for long-term maintainability, even if it comes at the cost of not being able to do as quick of rapid prototyping as a dynamically typed language (like Ruby or JS).

But then languages like Swift just mentioned have the Any type, and Rust has Any too, "a trait to emulate dynamic typing". Why are these added to these languages? What problems do they solve? Are they necessary for statically typed languages? If you don't have an Any type, what problems will you face?

Basically I'm wondering, to solve my problem of having 1.5 megabytes of type information in the output JavaScript build file (if I include dynamic typing / reflection capabilities in the custom lang), should I just cut that feature (runtime type checking and including the type metadata in the runtime build)? Or is it a necessity for certain key situations? It would seem it's a necessity sometimes, because Rust and Swift have the Any type, and to do that it seems you'd need to include the type info in the runtime build. I don't have a deep understanding of how the Any type really works in Rust or Swift (my main language is JS/TS), so I'm not sure why it was deemed necessary to include.

If you don't need an Any type, what problems will you face? Wondering at the same time if it would have been better for Rust and Swift to avoid this feature, or if not, what it's solving.

$\endgroup$
7
  • 4
    $\begingroup$ Does C's void* (something that can refer to any object, but you have to separately keep track of what type it is, because the language doesn't) count as an "Any" type? $\endgroup$
    – dan04
    Jul 6, 2023 at 20:26
  • $\begingroup$ I'm fairly sure Swift only has Any because it needed some way to represent Objective-C methods that take and return id. (Ironically, in modern Swift those no longer use Any, and instead AnyObject, which is different.) $\endgroup$
    – Bbrk24
    Jul 6, 2023 at 20:28
  • 3
    $\begingroup$ You absolutely do not (many FP languages, such as Haskell and OCaml, don't). Even Java makes a distinction between objects and primitives. $\endgroup$
    – user
    Jul 6, 2023 at 20:39
  • $\begingroup$ @mousetail std::any? $\endgroup$
    – phyrfox
    Jul 6, 2023 at 20:39
  • 1
    $\begingroup$ Python has a type object that is the superclass of all types. PEP 483 has a section describing how Any contrasts with object to allow for gradual typing. $\endgroup$
    – chepner
    Jul 7, 2023 at 19:23

5 Answers 5

20
$\begingroup$

Any is commonly known as a Top type: a type that is a super-type of every type.

Some strongly typed languages have quite restricted notions of sub-typing, so the concept of top type makes no sense there. For example, languages like SML and Idris allow sub-typing of type variables but not of defined types.

Sub-typing functional languages tend to have top types, but many others don't.

For example, Java. It has a java.lang.Object type that is the super-type of all reference types like java.lang.Integer but is not the super-type of value types like int, and its void type is a special type that can only appear in return position.

Java does have an auto-boxing system; you can often use an int where a java.lang.Integer is expected and vice-versa, but there are subtleties.

But that doesn't mean you can treat int as if it were a sub-type of java.lang.Object via java.lang.Integer. In that case, you would need to treat an array like int[] as a sub-type of java.lang.Integer[] which would cause problems because those two types are actually very different.

JVM languages like Scala and Kotlin do manage to have a top type, as you noted, but they do that by being very careful around how their array types map to JVM array types.

$\endgroup$
14
  • 5
    $\begingroup$ Not related to the actual question, but arrays in general are kinda scuffed in Java. You can treat a String[] as an Object[] at compile-time, but it'll blow up if you try to put in Objects into that casted String[] at runtime $\endgroup$
    – user
    Jul 6, 2023 at 22:21
  • 5
    $\begingroup$ it's more that Java's entire type system is horrible. the whole int vs integer because they couldn't figure out how to represent objects without tons of pointers is a horrible mess $\endgroup$ Jul 7, 2023 at 5:15
  • $\begingroup$ In Swift, "Any" is a protocol, not a type. A variable declared as "Any" has a type, but you don't know which one, except you know it follows the "Any" protocol. $\endgroup$
    – gnasher729
    Jul 7, 2023 at 8:57
  • $\begingroup$ @gnasher729 are Swift protocols existential? $\endgroup$ Jul 7, 2023 at 20:15
  • 2
    $\begingroup$ @MikeSamuel: IMHO, Java's mistake was failing to have an abstract "readable array" type with mutable and immutable subclasses (with the latter having a variety of constructors and factory methods that wouldn't expose a reference to the completed object until all members were populated). A limited form of multiple inheritance should be supported here, since even though neither ImmutableArray<Animal> nor ReadableArray<Cat> is a subtype of the other, but both should be viewed as supertypes of ReadableArray<Animal>. $\endgroup$
    – supercat
    Jul 9, 2023 at 17:02
12
$\begingroup$

Any enables dynamic typing and type erasure in a statically-typed language.

This turns out to be very useful, to the point where even Haskell and SML (languages with strong type guarantees and no subtyping) have Any equivalents or people working on them. And as mentioned, Rust has std::any which relies on a compiler extension: the compiler gives runtime-accessible tags to every 'static type for the sole purpose of supporting it.

  • Reflection AKA runtime metaprogramming is the main reason for dynamic types in statically-typed language. here are some meta-programming operations which are impossible at compile type, specifically ones which access metadata in private or dynamically-linked libraries. And even when the metaprogramming is possible at compile time, it's often easier to implement at runtime via reflection, because you can implement it in the language itself without creating a multi-phase compiler (a compiler which interprets its own code during compilation).

    • Reflection is one of the big reasons Java is so popular (see reflection examples; also one of the first sentences in the wikipedia, "The Java runtime provides dynamic capabilities (such as reflection and runtime code modification) that are typically not available in traditional compiled languages"). It's also one of the main benefits of dynamic languages like JavaScript, Python, and R over static languages like C/C++ and Haskell.

    • A big example where reflection comes in handy is debugging. Sometimes even in static languages, you just want to print something, particularly a datatype, but sometimes the datatype doesn't have Debug, Show, or any other toString equivalent. In this case reflection is your only option. This is the main reason for Swift's Mirror, a structure which, like Rust's Any, relies on a compiler extension.

  • The expression problem is another reason why Any and downcasting, and specifically polymorphism, are popular. A version of the expression problem states that it's easy for a language to allow users to add new methods to an existing type (if the type is an ADT like an enum) or new overloads to an existing method (if the type is a non-sealed class), but it's tricky to cleanly allow both. Any provides a messy solution to allow both: you can add new methods to a non-sealed class which downcast self to Any and then try upcasting to every known subclass, and you can add new overloads to the class as usual, with the additional stipulation that you handle these new subclasses in your methods which downcast to Any.

Ultimately, not all programs can be conveniently represented with static types. Notice I said "conveniently" because as some of the answers point out, you theoretically can encode any program in a statically-typed language, but only by inventing your own dynamic types e.g. by liberally using enums and failable functions. Furthermore, as the first answer points out, type-checking non-trivial properties requires formal proofs, which are extremely tedious). At these points (when you need enums and failable functions and/or formal proofs), it's much easier to simply upcast the type to "Top" and then downcast at runtime, asserting that the types are ok without a formal proof.

$\endgroup$
9
$\begingroup$

Dependency Injection

I wrote a dependency injection library in Swift. It makes use of as! and Any.

I'm not going to go into a detailed description of what DI is and when to use it, but the idea is that you can ask the DI container for an instance of FooProtocol (IFoo for the C# devs out there) and it gives you one. You don't need to worry about where it comes from, and it can easily be changed (e.g. for testing).

In order for this to be possible, the container needs to store, as a vast oversimplification, a bunch of type-instance pairs. However, you can't use <T>Dictionary<T.Type, T> for a number of reasons. Beyond the fact that that kind of generic container doesn't exist in the language, this doesn't facilitate lazy initialization or weak references.

Surely, I thought, this should be possible! Just start with a protocol:

protocol Registration {
  associatedtype T
  func resolve() -> T
}

and then store an array, [any Registration].

Let's try to implement the lookup function given this.

private var registrations: [any Registration]

func resolve<T>() -> T {
  // I'm using .lastIndex rather than .firstIndex for reasons not important here
  let index = registrations.lastIndex(where: { $0.T == T })!
  return registrations[index].resolve()
}

Seems legit. Except, wait a second, how does that work? The answer is, it doesn't. Go ahead, try it yourself if you don't believe me.

There are really two problems here:

  1. By using lastIndex + subscript, the type system has no way of verifying that resolve() returns the correct type. Indeed, the subscript returns an any Registration, so Swift marks resolve() as returning Any. This is arguably a Swift-specific implementation problem, except that...
  2. Determining whether $0.T == T relies on the exact kind of type information you want to eliminate!

There are more reasons I chose to do it this way, such as being backwards-compatible with earlier versions than iOS 16.

I tried for weeks to eliminate as! and Any from that codebase, all to no avail. Some of it was restricted by limitations of Swift itself, but even if I managed to remove all explicit casts, there'd still have to be type comparisons somewhere.

$\endgroup$
6
$\begingroup$

Any is mostly useful in the libraries that internally need to work with arbitrary data records, like some "user data object" on some library - managed data structure. It is the further evolution of the smart pointer:

  • A smart pointer knows how to safely delete the object.
  • Any also knows about the type of the object, and how to copy it.

If you have knowledge about the type being stored, Any is not the proper tool. If all possible types that could be stored are known, a Variant is a better container for them, and for the single type value that may also be missing, an Optional may be offered.

$\endgroup$
1
$\begingroup$

The accepted answer already explains that you do not need it in your language. However, you'll need it in your compiler to get to a somewhat sane architecture. Otherwise, you wouldn't be able to process something like "true ? new Object : 7" in Java 1.4 or your compiler would have to quit on first error. If you have it anyway in your compiler, you can expose it to the user.

However, in languages that are close to the hardware, i.e. the representation of int is something like 4 bytes that can reside in a register instead of having everything boxed on the heap, there is no way to represent any. Thus, such a language might have any, but it wouldn't allow you to use it everywhere.

$\endgroup$
7
  • 2
    $\begingroup$ I don't see how that claim about the compiler follows at all. Compilers with great error checking have been written in C and Ada, which don't have an Any type. $\endgroup$
    – prosfilaes
    Jul 8, 2023 at 9:30
  • $\begingroup$ The language used to implement the compiler is not relevant. I'm talking about a data structure or type representation for any in the processed language. $\endgroup$
    – feldentm
    Jul 8, 2023 at 9:32
  • 2
    $\begingroup$ The argument from compiler design is completely fallacious. A compiler can handle ill-typed expressions like that in many other ways — e.g. tagging them as a type error. “Type them as ‘Any’, or quit immediately” is a massively false dichotomy. And “If you have it anyway in your compiler, you can expose it to the user.” is just as dubious — compilers do many things (such as handling possibly ill-typed code) which you want to be outlawed in the object language, exactly so that the object language can provide type-safety guarantees. $\endgroup$ Jul 8, 2023 at 17:44
  • $\begingroup$ "Tag it as error" is the same thing as introducing an any type to a language that doesn't have one as you need to introduce some processable data to continue there. You could also call that type error or poison or whatever, but it will always be an any type. While I agree that "expose what you have" is dubious from design perspective, it is how things work from my experience. $\endgroup$
    – feldentm
    Jul 9, 2023 at 7:34
  • 2
    $\begingroup$ An error type has no dynamic semantics; that is wildly different from an Any type. $\endgroup$
    – prosfilaes
    Jul 10, 2023 at 19:59

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .