Few or many features by default

Hi all,

I've been working on a simple crate for text wrapping and when I saw that there is a cool hyphenation crate on crates.io, I added support for it. Your command line programs can now have automatically hyphenated and word-wrapped help output :slight_smile:

Unfortunately, the hyphenation crate pulls in a number of dependencies and takes about a minute to compile. So making this dependency optional seems like a reasonable thing to do in case people don't care for the hyphenation.

My question is now if I should make hyphenation a default feature of the textwrap crate, or if I should make it opt-in? Is there a rule of thumb in the Rust world about this?

I guess the question is how easy it is for users to opt-in and opt-out, an in particular how easy it is to change this if my textwrap crate is an indirect dependency? I found this topic about this — there the conclusion seems to be that there's no easy way for users to opt-in to an optional feature while being sure that they use the same version as the library in question.

Also, as I understand it, it's a little easier to opt-in as a user: you simply specify the features you want in addition to the default features of a crate:

[dependencies.awesome]
version = "1.3.5"
features = ["cool-feature"]

Opting out is harder since you cannot just subtract a feature from the list of default features:

[dependencies.awesome]
version = "1.3.5"
default-features = false
features = ["lots", "of", "base", "features"]

(In my case, simply enabling hyphenation on it's own won't do anything since the user would also need to select the hyphenation patterns to use. So if textwrap is an indirect dependency, you can only use hyphenation if your direct dependency has a way of letting you specify a hyphenation pattern.)

Thanks for any insights!

There probably isn't a clear answer here. It's a trade-off between minimal and maximal functionality, overhead vs convenience etc.

There is another way to make hyphenation optional other than Cargo features — make it a separate crate, and only expose an interface to integrate:

let mut wrapper = Wrapper::new(80);
wrapper.hyphenate_with(Hyphenator::new());

That could probably be extended to other problems like support for rfc2646 format=flowed.

1 Like

It's not a great answer, but I try to follow two-ish rules...kinda.

  • Rust (unofficially?) prefers opt-in, especially if there is a somewhat high associated cost (execution time, compile time, etc.)
  • I try to only worry about my direct users and not indirect users
  • The only time I use opt-out is when the vast majority of direct users want the functionality and there ins't a huge impact to either compile or execution time
1 Like

Thanks for the advice!

Yeah, that seems like a very reasonable rule of thumb :slight_smile:. And as you pointed out on GitHub, the hyphenation functionality brings with it a bunch of dependencies and it might not be used by most users of the crate (since using it implies asking the user for the language of the text being wrapped).

That is exactly what I hope to do. I gave my own Wrapper struct an Option<Corpus> field, with the idea that users can opt-in to hyphenation by putting Some(corpus) there.

However, as far as I understand, this means that my crate must depend on the hyphenation crate and any user of textwrap will end up paying for this (in terms of compile time, mostly).

I'm now playing with making it an optional Cargo feature. If the feature is not defined, the textwrap crate defines a dummy or empty Corpus struct so that the Option<Corpus> field still works:

#[cfg(feature = "hyphenation")]
pub use hyphenation::Corpus;

#[cfg(not(feature = "hyphenation"))]
pub struct Corpus { }

This allows me to keep my Wrapper struct unchanged, which seems nice. However, maybe it's better to not define the corpus struct field at all in this case? I'm still trying to figure out how to most cleanly do this :slight_smile:

You don't need to directly depend on the hyphenation crate. You can define an abstract interface (a trait) in your wrapping crate for other crates to implement.

Then make a wrapping-hyphenation-plugin crate that depends on both the wrapping crate (for the trait) and hyphenation crate, and implements the trait.

1 Like

Sorry about not getting back sooner and thank a lot for that idea! :slight_smile:

If I understand you correctly, the advantage of this would be that the textwrap crate would be written without any

#[cfg(feature = "hyphenation")]

lines at all. So no conditional compilation since hyphenation is expressed in terms of a trait.

Another crate, perhaps called textwrap-hyphenation, would then implement the trait. This crate would depend on both textwrap and hyphenation. The textwrap-hyphenation crate would only export the implementation of the trait from textwrap. There would also not be any conditional compilation in the textwrap-hyphenation crate.

A variant on this would be to avoid an extra crate and still use an optional Cargo feature. If the feature is defined, the textwrap crate would depend on hyphenation and provide an implementation of the trait — if not, only the trait would be defined and no implementation would be given. This effectively inlines the hypothetical textwrap-hyphenation crate into the textwrap crate.