Hi swift-dev,

I talked to a few people about this problem and we agreed that it is a problem 
and that it needs to be discussed. I didn't quite know where it would fit best 
but let's go with swift-dev, please feel free to tell to post it elsewhere if 
necessary. And apologies for the long mail, couldn't come up with a sensible 
tl;dr...

Let me briefly introduce the problem what for the lack of a better name I call 
'signature package' or 'Service Provider Interface' (SPI) as some people from 
the Java community seem to be calling it 
(https://en.wikipedia.org/wiki/Service_provider_interface). For the rest of 
this email I'll use the term SPI.

In a large ecosystem there is a few pieces that many libraries will depend on 
and yet it seems pretty much impossible to standardise exactly one 
implementation. Logging is a very good example as many people have different 
ideas about how logging should and should not work. At the moment I guess your 
best bet is to use your preferred logging API and hope that all your other 
dependencies use the same one. If not you'll likely run into annoying problems 
(different sub-systems logging to different places or worse).

Also, in a world where some dependencies might be closed source this is an even 
bigger problem as clearly no open-source framework will depend on something 
that's not open-source.


In Java the way seems to be to standardise on some logging interface (read 
`protocol`) with different implementations. For logging that'd probably be 
SLF4J [4]. In Swift:

    let logger: LoggerProtocol = MyFavouriteLoggingFramework(configuration)

where `LoggerProtocol` comes from some SPI package and 
`MyFavouriteLoggingFramework` is basically what the name says. And as a general 
practise, everybody would only use `LoggerProtocol`. Then tomorrow when I'll 
change my mind replacing `MyFavouriteLoggingFramework` by 
`BetterFasterLoggingFramework` does the job. With 'dependency injection' this 
'logger' is handed through the whole program and there's a good chance of it 
all working out. The benefits are that everybody just needs to agree on a 
`protocol` instead of an implementation. 👍

In Swift the downside is that this means we're now getting a virtual dispatch 
and the existential everywhere (which in Java will be optimised away by the 
JIT). That might not be a huge problem but it might undermine 
'CrazyFastLoggingFramework's adoption as we always pay overhead.

I don't think this problem can be elegantly solved today. What I could make 
work today (and maybe we could add language/SwiftPM support to facilitate it) 
is this (⚠️, it's ugly)

- one SwiftPM package defines the SPI only, the only thing it exports is a 
`public protocol` called say `_spi_Logger`, no implementation
- every implementation of that SPI defines a `public struct Logger: 
_spi_Logger` (yes, they all share the _same_ name)
- every package that wants to log contains

    #if USE_FOO_LOGGER
        import FooLogger
    #elif USE_BAR_LOGGER
        import BarLogger
    #else
        import BuzLogger
    #endif

  where 'BuzLogger' is the preferred logging system of this package but if 
either `USE_FOO_LOGGER` or `USE_BAR_LOGGER` was defined this package is happy 
to use those as well.
- `Logger` is always used as the type, it might be provided by different 
packages though
- in Package.swift of said package we'll need to define something like this:

     func loggingDependency() -> Package.Dependency {
     #if USE_FOO_LOGGER
         return .package(url: "github.com/...../foo.git", ...)
     #elif USE_BAR_LOGGER
         return ...
     #else
         return .package(url: "github.com/...../buz.git", ...)
     #endif
     }

      func loggingDependencyTarget() -> Target.Dependency {
     #if USE_FOO_LOGGER
         return "foo"
     #elif USE_BAR_LOGGER
         return "bar"
     #else
         return "buz"
     #endif
     }
- in the dependencies array of Package.swift we'll then use 
`loggingDependency()` and in the target we use `loggingDependencyTarget()` 
instead of the concrete one

Yes, it's awful but even in a world with different opinions about the 
implementation of a logger, we can make the program work.
In the happy case where application and all dependency agree that 
'AwesomeLogging' is the best framework we can just type `swift build` and 
everything works. In the case where some dependencies think 'AwesomeLogging' is 
the best but others prefer 'BestEverLogging' we can force the whole application 
into one using `swift build -Xswiftc -DUSE_AWESOME_LOGGING` or `swift build 
-Xswiftc -DUSE_BEST_EVER_LOGGING`.


Wrapping up, I can see a few different options:

1) do nothing and live with the situation (no Swift/SwiftPM changes required)
2) advertise something similar to what I propose above (no Swift/SwiftPM 
changes required)
3) do what Java does but optimise the existential away at compile time (if the 
compiler can prove there's actually only one type that implements that protocol)
4) teach SwiftPM about those SPI packages and make everything work, maybe by 
textually replacing the import statements in the  source?
5) do what Haskell did and retrofit a module system that can support this
6) have 'special' `specialized protocol` for which a concrete implementation 
needs to be selected by the primary source
7) something I haven't thought of

Btw, both Haskell (with the new 'backpack' [1, 2]) and ML have 'signatures' to 
solve this problem. A signature is basically an SPI. For an example see the 
backpack-str [3] module in Haskell which defines the signature (str-sig) and a 
bunch of different implementations for that signature (str-bytestring, 
str-string, str-foundation, str-text, ...).

Let me know what you think!

[1]: https://plv.mpi-sws.org/backpack/
[2]: https://ghc.haskell.org/trac/ghc/wiki/Backpack
[3]: https://github.com/haskell-backpack/backpack-str
[4]: https://www.slf4j.org

-- Johannes
PS: I attached a tar ball which contains the following 6 SwiftPM packages that 
are created like I describe above:

- app,      the main application, prefers the 'foo' logging library
- somelibA, some library which logs and prefers the 'foo' logging library
- somelibB, some other library which prefers the 'bar' logging library
- foo,      the 'foo' logging library
- bar,      the 'bar' logging library
- spi,      the logging SPI

The dependency default graph looks like this:
      +- somelibA ---+ foo
     /              /      \
app +--------------/        +-- spi
     \                     /
      +- somelibB ---- bar

that looks all good, except that 'foo' and 'bar' are two logging libraries 🙈. 
In other words, we're in the unhappy case, therefore just typing `swift build` 
gives this:

--- SNIP ---
-1- johannes:~/devel/swift-spi-demo/app
$ swift build
Compile Swift Module 'app' (1 sources)
/Users/johannes/devel/swift-spi-demo/app/Sources/app/main.swift:14:23: error: 
cannot convert value of type 'Logger' to expected argument type 'Logger'
somelibB_func(logger: logger)
                      ^~~~~~
error: terminated(1): 
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-build-tool
 -f /Users/johannes/devel/swift-spi-demo/app/.build/debug.yaml main
--- SNAP ---

because there's two `Logger` types. But selecting `foo` gives (note that all 
lines start with 'Foo:'):

--- SNIP ---
$ swift build -Xswiftc -DUSE_FOO
Compile Swift Module 'spi' (1 sources)
Compile Swift Module 'foo' (1 sources)
Compile Swift Module 'bar' (1 sources)
Compile Swift Module 'somelibB' (1 sources)
Compile Swift Module 'somelibA' (1 sources)
Compile Swift Module 'app' (1 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/app
$ ./.build/x86_64-apple-macosx10.10/debug/app
Foo: info: hello from the app
Foo: info: hello from somelibA
Foo: info: hello from somelibB
Foo: info: hello from somelibA
Foo: info: hello from somelibB
--- SNAP ---

and for 'bar' (note that all lines start with 'Bar:')

--- SNIP ---
$ swift build -Xswiftc -DUSE_BAR
Compile Swift Module 'spi' (1 sources)
Compile Swift Module 'foo' (1 sources)
Compile Swift Module 'bar' (1 sources)
Compile Swift Module 'somelibA' (1 sources)
Compile Swift Module 'somelibB' (1 sources)
Compile Swift Module 'app' (1 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/app
$ ./.build/x86_64-apple-macosx10.10/debug/app
Bar: info: hello from the app
Bar: info: hello from somelibA
Bar: info: hello from somelibB
Bar: info: hello from somelibA
Bar: info: hello from somelibB
--- SNAP ---

Attachment: swift-spi-demo.tar.gz
Description: GNU Zip compressed data



_______________________________________________
swift-dev mailing list
swift-dev@swift.org
https://lists.swift.org/mailman/listinfo/swift-dev

Reply via email to