I added a similar type — “non-empty-string” to a typechecker for PHP, and it’s been adopted widely in the PHP ecosystem. It turns out to be pretty handy, especially when there’s a full type system to support it.
Turns out it's some kind of bags and accessories brand!
https://www.typescriptlang.org/docs/handbook/utility-types.h...
Edit: nevermind, LLM fooled me.
I see what you did there
https://www.typescriptlang.org/play/?#code/C4TwDgpgBAcg9gOwK...
This post is a Haskell koan. We’ll get to the background and motivation, but the goal here is to share a small and uncommon technique that we’ve employed, and enjoyed; perfect blog fodder.
In short, we wrote a type-checked non-empty string constructor, replaced thousands of equivalent TemplateHaskell calls, for a ~10% build-time improvement in a large and data-heavy package that made many calls to it.
before, after, invalidBefore, invalidAfter :: NonEmptyText
before = $$(NonEmptyText.make "hello")
after = NonEmptyText.make "hello"
invalidBefore = $$(NonEmptyText.make "") -- ⇝ error during splice evaluation ...
invalidAfter = NonEmptyText.make "" -- ⇝ type error: expected a non-empty string
To make invalid states unrepresentable is a core design goal of the software at Bellroy. With that in mind, a type we often employ for textual data – of which we have a lot – is NonEmptyText. It is just what it sounds like: a value of this type is a string with at least one character.
The technique is a result of a convergence of GHC features over the past 15 years or so. In particular, RequiredTypeArguments, introduced in GHC 9.10, lets us pass a type-level string literal into a function as if it were a value. We can throw a custom type error message like "Expected a non-empty string" if we (at the type-level) see an empty string; like so:
type family IsNonEmptySymbol symbol :: Constraint where
IsNonEmptySymbol "" = Unsatisfiable (Text "Expected a non-empty string")
IsNonEmptySymbol _ = (()::Constraint) -- empty constraint is always satisfied
-- Note previous syntax without RequiredTypeArguments:
-- make :: forall symbol. IsNonEmptySymbol symbol => NonEmptyText
-- with usage like `make @"hello!"`
make :: forall symbol -> (IsNonEmptySymbol symbol) => NonEmptyText
make symbol = NonEmptyText (fromString (symbolVal (Proxy :: Proxy symbol)))
test :: NonEmptyText
test = make "hello!"
Which, with the right LANGUAGE incantations, does actually work. This requires UndecidableInstances to work, which is not harmful in and of itself, but does open up the possibilities of what can go wrong1.
Additionally, since IsNonEmptySymbol is a type family, it can’t directly be used like an ordinary typeclass constraint – for instance, it can’t be packaged into a Dict, or used with functions like Data.SOP.hcfoldMap; it’s not something you can just “ask for an instance of” like an ordinary typeclass, despite returning a Constraint like one.
The final step then to this trick is writing IsNonEmptySymbol as a typeclass:
class IsNonEmptySymbol symbol
instance {-# OVERLAPPING #-} Unsatisfiable (Text "Expected a non-empty string") => IsNonEmptySymbol ""
instance IsNonEmptySymbol a
-- make: same as above
make :: forall symbol -> (IsNonEmptySymbol symbol) => NonEmptyText
make symbol = NonEmptyText (fromString (symbolVal (Proxy :: Proxy symbol)))
When GHC resolves the IsNonEmptySymbol constraint given an empty string, it finds both: _ => instance IsNonEmptySymbol "" and instance IsNonEmptySymbol a. If we omitted the OVERLAPPING pragma, that would be where GHC raises an error; it wouldn’t know which instance to actually choose, and would complain that there are overlapping possibilities – which is fine, since the only case where there is an overlapping instance is in the case we want to forbid, when the input is "".
So the effect of the OVERLAPPING pragma here is that GHC is does choose the instance we “want”; the one with the custom type error. The instance then raises our custom error message informing the user that a non-empty string was expected.
Our internal bellroy-data package – containing data such as information about known freight and shipping providers, accounting systems, product data, tax codes, etc. – had thousands of TH splices like $$(NonEmptyText.makeTH _). Moving to the RequiredTypeArgument approach shaved off around 10% of the compilation time for that package.
We can re-use (almost) the exact same code to validate that a given Natural is positive to make a type-checked Positive constructor. In general we can use this technique for any type we can define a type-level predicate for.
From here you might be able to imagine, for instance, type-level string parsing to define for instance type-safe term syntax for constructing URIs: it will work, but you’ll run into GHC’s default reduction limit of 20 quite quickly; an O(n) type-level validator over a n-length string has a hard-cap of 20 “reduction” (i.e., parsing) steps and thus parsing cannot proceed beyond 20 characters – that is, also assuming your parsing steps themselves aren’t counting towards that limit.
In general non-trivial algorithms are also quite awkward to express in type families; you have no way to write let-bindings for instance, or do pattern matching with a case-like syntax: if you need either, they must be expressed with some combination of extra type arguments and helper type families. This also needs a bit of plumbing to work as a typeclass as in the above IsNonEmptySymbol class we created.
Nevertheless, if you’ve gotten this far, you’re probably interested in what that can look like: behold 🪄, type-level parsing for DynamoDB table-names:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE QuantifiedConstraints #-}
{-# LANGUAGE RequiredTypeArguments #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
import Data.Proxy
import Data.String (fromString)
import Data.Text (Text)
import Data.Type.Bool qualified as Bool
import GHC.TypeError
import GHC.TypeLits
-- | Valid DynamoDB table names must match regex /^[a-zA-Z_.-]{3,255}$/
newtype TableName = TableName Text
deriving (Show)
make :: forall name -> (IsValidTableName name) => TableName
make name = TableName (fromString (symbolVal (Proxy :: Proxy name)))
So far, so good. But enumerating every single invalid string will take some time; we need an algorithmic approach rather than how IsNonEmptySymbol worked above. As above, we’d prefer to encapsulate this into a typeclass of kind Type -> Constraint. The approach I use here is to use a type-family to direct the resolution of an inner typeclass. So this approach works as a unary typeclass (desirable) and surfaces custom type error messages to the programmer (excellent).
class (KnownSymbol a) => IsValidTableName a
instance (KnownSymbol a, IsValidTableName_ validity a) => IsValidTableName a
-- | Wrapper class which produces nice error messages
--
-- The `wasValid` parameter is computed by the `IsValidTableName__` type
-- family. We can then direct GHC into either a no-op instance (success),
-- or an instance which throws a somewhat informative error.
class (IsValidTableName__ a ~ wasValid) => IsValidTableName_ (wasValid :: Bool) a
instance (IsValidTableName__ a ~ 'True) => IsValidTableName_ 'True a
instance (Unsatisfiable ('Text "Encountered invalid TableName")) => IsValidTableName_ 'False a
Now we actually need to implement this IsValidTableName__ type family which looks like IsValidTableName__ (input :: Symbol) :: Bool.
type IsValidTableName__ text = IsValidTableName_go 0 'Nothing (UnconsSymbol text)
-- inner loop for IsValidTableName__
type family IsValidTableName_go (len :: Nat) (invalidLastChar :: Maybe Char) (unconsResult :: Maybe (Char, Symbol)) :: Bool where
IsValidTableName_go len 'Nothing ('Just '(x, xs)) = IsValidTableName_go (len + 1) (InvalidTableChar x) (UnconsSymbol xs)
IsValidTableName_go len 'Nothing _ = (3 <=? len) Bool.&& (len <=? 255)
IsValidTableName_go len ('Just invalidChar) _ = 'False
-- check individual characters for validity
type family InvalidTableChar (ch :: Char) :: Maybe Char where
InvalidTableChar ch = Bool.If (IsValidTableChar ch) 'Nothing ('Just ch)
type family IsValidTableChar (ch :: Char) :: Bool where
IsValidTableChar '-' = 'True
IsValidTableChar '_' = 'True
IsValidTableChar '.' = 'True
IsValidTableChar ch =
('a' <=? ch Bool.&& ch <=? 'z')
Bool.|| ('A' <=? ch Bool.&& ch <=? 'Z')
Bool.|| ('0' <=? ch Bool.&& ch <=? '9')
Finally, we can actually use this:
valid, invalid, invalid2 :: TableName
valid = make "hello-bellroy123"
invalid = make "no" -- error! "Encountered invalid TableName"
invalid2 = make "tablename!!" -- error! "Encountered invalid TableName"
The singletons-th package could be used here to promote actual term-level functions like the IsValidTableName function, but a point here is to demonstrate how that would actually work under the hood.
There is an inexorable march towards Dependent Haskell which is slowly-but-surely inching closer to reality with every major GHC release. Languages like Idris and Lean have it today, but for better or worse, Haskell is the language in this space with the most adoption and inertia. GHC developers: please continue! 🙂
UndecidableInstances and details a use-case where it is quite dangerous.↩︎You can do this trick for type-checking emptiness of string literals
https://www.typescriptlang.org/play/?jsx=0#code/C4TwDgpgBAsg...
declare const brand: unique symbol;
type NonEmptyString = string & { readonly [brand]: 'NonEmptyString' };
// the ONLY non-cast way to produce one
export function nonEmptyString(s: string): NonEmptyString | undefined {
return s.length > 0 ? (s as NonEmptyString) : undefined;
}
export type { NonEmptyString };The "whale" users who account for a disproportionately large percentage of an astrologer's revenue tend to know the factual information surrounding their birth fairly well. An app/astrologer who doesn't get these facts right, even for a handful of clients, will get a bad reputation fairly quickly.
I reckon the same principle would hold in cultural bubbles where reading tea leaves is a customary means of divination. If the client recognizes recognize black tea, but the fortuneteller insists it is rooibos, there won't be much trust in the rest of the prophecy.
Advertising that the horoscope shop uses Haskell is actually a solid business idea. It pre-filters for the sort of dev who will be able to do the math.
Alright, you're on - post the place you were born, including date and time and I'll cast your chart and tell you what I see. Then you can tell me if you think it's bullshit or not.
Free reading, you have nothing to lose.
Example: Milpitas, CA @ 4:52am January 24 1987
You won't (write software to) do that without knowing a whole bunch of linear algebra and ODEs.
The fortune-telling part is not what needs the math degree.
The average Visual Basic programmer and the world's best mathematician are going to be about equally good at writing the fortune-telling part, but the mathematician will have a much easier time getting the factual part right.
My impression (not interested in pseudo sciences so I haven't looked into this hard) was that Astrology froze before Kepler explains how the planets move, and so the model used by astrologers is just wrong and won't actually tell you what was in the sky when you were born for example?
You don't need Newton (who figured out why they do that) but you do need Kepler or your physical data will be wrong surely? It won't matter because this isn't actually a way to divine the future, but it will be wrong AIUI
Modern ascendant calculations are "correct", in the sense that they'll match what you see in the sky. Apps like AstroMatrix work from NASA data.
As you rightly observed, it doesn't make a difference when it comes to foretelling the future. But it does make a difference if your aim is to make money selling it. The big spenders on these applications often care a great deal about getting the particulars just so.
The sign that a planet is in, which house, and aspects between the planets are what enables astrological fortune-telling and prediction (the forecasting aspect of astrology is technically known as transits and progressions, this is the "predicting the future" piece).
The bedside manner of the astrologer obviously has an enormous influence. One astrologer will look at a chart and treat it as a fixed "you are this way because of your chart" as if you can't change. A more thoughtful (read: actually good) astrologer will treat the chart as an evolutionary blueprint for growth and change. In many ways astrology is really just a different version of psychology and counseling.
A great book to read if you're open-minded but also don't want a bunch of "woo" is to check The Inner Sky by Steven Forrest. The first chapter of the book is called "Why Bother?" For what it's worth I had my chart read by Steven and it has really helped me in my life.