[swift-evolution] [Proposal] Foundation Swift Archival & Serialization

Brent Royal-Gordon brent at architechies.com
Fri Mar 17 17:13:59 CDT 2017


> On Mar 17, 2017, at 2:38 PM, Matthew Johnson <matthew at anandabits.com> wrote:
> 
>> At a broad level, that's a good idea. But why not provide something more precise than a bag of `Any`s here? You're in pure Swift; you have that flexibility.
>> 
>> 	protocol Codable {
>> 		associatedtype CodingContext = ()
>> 		
>> 		init<Coder: Decoder>(from decoder: Coder, with context: CodingContext) throws
>> 		func encoder<Coder: Encoder>(from encoder: Coder, with context: CodingContext) throws
>> 	}
>> 	protocol Encoder {
>> 		associatedtype CodingContext = ()
>> 		
>> 		func container<Key : CodingKey>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key, CodingContext>
>>>> 	}
>> 	class KeyedEncodingContainer<Key: CodingKey, CodingContext> {
>> 		func encode<Value: Codable>(_ value: Value,? forKey key: Key, with context: Value.CodingContext) throws { … }
>> 		
>> 		// Shorthand when contexts are the same:
>> 		func encode<Value: Codable>(_ value: Value,? forKey key: Key) throws
>> 			where Value.CodingContext == CodingContext
>> 		{ … }
>> 		
>>>> 	}
> 
> This is sort of similar to the design I suggested for contexts.  The difference is that you’re requiring all Codable to be context aware and by introducing an associated type you break the ability to use Codable as an existential.

I don't think banning existentials is actually a loss. Since `encode(_:)` doesn't record type information, and instead `decode(_:)` requires the exact concrete type to be passed in, `Codable` existentials cannot be usefully encoded or decoded. For instance, a heterogeneous `[Codable]` would encode in several different, probably mutually incompatible formats, without any type information that could distinguish between them. Since the only semantics of `Codable` are encoding and decoding, and decoding is always done by an `init`, `Codable` existentials are useless and we lose nothing by not supporting them.

> Many Codable conforming types won’t need to know anything about a context.  I would still want to be able to encode them along with my custom context-aware types.  A good example is types from Foundation that will conform to Codable.  They will definitely not know anything about my context but I still want to be able to encode a URL alongside my custom context-aware types.

Sure; you can do that by calling `encode(_:forKey:with:)` and passing a freshly-made `()` context. We might even add a second convenience overload of `encode(_:forKey:)`:

	class KeyedEncodingContainer<Key: CodingKey, CodingContext> {
		func encode<Value: Codable>(_ value: Value,? forKey key: Key, with context: Value.CodingContext) throws { … }
		
		// Shorthand when contexts are the same:
		func encode<Value: Codable>(_ value: Value,? forKey key: Key) throws
			where Value.CodingContext == CodingContext
		{
			try encode(value, forKey: key, with: currentContext)
		}
		
		// Shorthand when the type uses a Void context:
		func encode<Value: Codable>(_ value: Value,? forKey key: Key) throws
			where Value.CodingContext == Void
		{
			try encode(value, forKey: key, with: ())
		}
		
		…
	}

The main disadvantage I can think of in this design is that even `Codable` users who don't need a context have to have a `with context: Void` in their code. This might be confusing to new developers, but I think it's worth it.

(I don't think I mentioned this anywhere, but containers like `Array` should take on the `CodingContext` of their `Element`s and pass the context they receive through without examining it. That would probably be pretty common with generic container types.)

> Did you take a look at the design I suggested?  What do you think of it?

I think that, if a type wants to support context-free coding, it should use an optional `CodingContext`. :^)

In all seriousness, I see the design as very slightly weak, in that it makes it easy to forget to pass a context through, but quite acceptable. It would certainly solve the `with context: Void` problem I mentioned. I might consider reversing the relationship between the two protocols, though:

	public protocol ContextAwareCodable {
		associatedtype CodingContext
		
		init(from decoder: Decoder, with context: CodingContext) throws
		func encode(to encoder: Encoder, with context: CodingContext) throws
	}
	public protocol Codable: ContextAwareCodable where CodingContext == Void {
		init(from decoder: Decoder) throws
		func encode(to encoder: Encoder) throws
	}
	extension Codable {
		public init(from decoder: Decoder, with context: Void) throws {
			try self.init(from: decoder)
		}
		func encode(to encoder: Encoder, with context: Void) throws {
			try encode(to: encoder)
		}
	}

Most `Encoder`/`Decoder` APIs would have to use `ContextAwareCodable`, but if you're writing a coder, you'd better be aware of contexts.

* * *

A thought I just had: Someone upthread mentioned that `Codable` might be better as part of the standard library. One reason to favor that approach is that you could then make `Codable` support a requirement of types like `BinaryInteger` and `FloatingPoint`.

It might still make sense to have the coders themselves be part of Foundation; only the protocols defining `Codable`, `Encoder`, `Decoder`, and their ancillary types would be part of the standard library.

-- 
Brent Royal-Gordon
Architechies

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170317/510e5c2d/attachment.html>


More information about the swift-evolution mailing list