Postgres uses snake_case. Tables should be plural (e.g. teachers,
students). Text-based enums are currently also snake_case (except domain
abbreviations and math system which are ALLCAPS), though this makes
interoperating with Haskell and JavaScript non-obvious.
If a table has a foreign key to another table, remove the product and subject prefix from the foreign key column's name unless doing so would introduce ambiguity.
Don't do this:
CREATE TABLE ela_adaptive_skill_practice_paragraphs (
id integer PRIMARY KEY NOT NULL,
ela_adaptive_skill_practice_content_id uuid
REFERENCES ela_adaptive_skill_practice_content NOT NULL,
type text NOT NULL,
content text NOT NULL,
caption text,
position integer NOT NULL,
UNIQUE (ela_adaptive_skill_practice_content_id, position),
CONSTRAINT valid_ela_adaptive_skill_practice_paragraph_position
CHECK (position >= 0)
);Do this:
CREATE TABLE ela_adaptive_skill_practice_paragraphs (
id integer PRIMARY KEY NOT NULL,
- ela_adaptive_skill_practice_content_id uuid
- REFERENCES ela_adaptive_skill_practice_content NOT NULL,
+ content_id uuid REFERENCES ela_adaptive_skill_practice_content NOT NULL,
type text NOT NULL,
content text NOT NULL,
caption text,
position integer NOT NULL,
UNIQUE (ela_adaptive_skill_practice_content_id, position),
CONSTRAINT valid_ela_adaptive_skill_practice_paragraph_position
CHECK (position >= 0)
);We only need to convey that this field is a pointer to "content". Any other information we could encode can be understood from context. Furthermore, the elided information is more likely to change. In fact, we no longer refer to this product as "AdaptiveSkillPractice". It's just "SkillsPractice" now. However, because we've encoded superfluous information in the column, the Haskell and JSON representations do too, which makes changing it a multi-step process.
Haskell uses camelCase for identifiers and TitleCase for types and
constructors. The latter is enforced by the language. Names that are being
ignored should either use the wildcard _ or specify what is being ignored
(e.g. _teacher, _standard).
Prefer short, single-letter names for polymorphic type variables and arguments. You'll notice a number of conventions with type variables.
Unconstrained type variables often start with a and continue alphabetically
id :: a -> a
const :: a -> b -> aPolymorphic lists are often "pluralized" by appending s
filter :: (a -> Bool) -> [a] -> [a]
filter _ [] = []
filter predicate (a:as)
| predicate a = a : bs
| otherwise = bs
where
bs = filter predicate asf is for Functor and sometimes Applicative and Alternative
fmap :: Functor f => (a -> b) -> f a -> f b
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
guard :: Alternative f => Bool -> f ()t might be Traversable or Foldable
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b
traverse :: (Applicative f, Traversable t) => (a -> f b) -> t a -> f (t b)m is almost always Monad or something monad-transformer related
when :: Monad m => Bool -> m () -> m ()
liftIO :: MonadIO m => IO a -> m as is for state and e is for environment
put :: MonadState s m => s -> m ()
ask :: MonadReader e m => m eThese are conventions only, but picking something unusual for a common typeclass might surprise other readers.
Avoid using single-letter names or abbreviations for values with a concrete type, especially if that type is domain-specific. To crib from the Swift API Design Guidelines, "Clarity is more important than brevity."
For example, don't do this:
fetchCourseMemberships :: MonadIO m => Entity Teacher -> SqlReadT m [Entity CourseMembership]
fetchCourseMemberships (Entity tId teacher) =
select $ from $ \(ts `InnerJoin` cs `InnerJoin` cms) -> do
on $ cs ^. CourseId ==. cms ^. CourseMembershipCourseId
on $ cs ^. CourseTeacherId ==. ts ^. TeacherId
where_ $
case teacherSchoolId teacher of
Just sId ->
ts ^. TeacherSchoolId ==. val (Just sId)
Nothing ->
ts ^. TeacherId ==. val tId
pure cmsDo this instead:
fetchCourseMemberships :: MonadIO m => Entity Teacher -> SqlReadT m [Entity CourseMembership]
fetchCourseMemberships (Entity teacherId teacher) =
select $ from $ \(teachers `InnerJoin` courses `InnerJoin` memberships) -> do
on $ courses ^. CourseId ==. memberships ^. CourseMembershipCourseId
on $ courses ^. CourseTeacherId ==. teachers ^. TeacherId
where_ $
case teacherSchoolId teacher of
Just schoolId ->
teachers ^. TeacherSchoolId ==. val (Just schoolId)
Nothing ->
teachers ^. TeacherId ==. val teacherId
pure membershipsWe tolerate a few abbreviations as components of identifiers (e.g. the num
portion of numQuestionsAnswered), but there doesn't seem to be any broad
consensus about this.
Database access functions should follow standard prefixes and suffixes for the following cases:
fetch{...}for a normalSELECTfetch{...}Exists(By{...})for aSELECT EXISTScount{...}for aCOUNT
And should follow a pattern that indicates:
- What it returns, specifically if it returns a whole resource or just one field
- What its arguments are, specifically if they are whole resources or fields
- A correct pluralization of arguments and returned values
{fetch|count}{Resource(Field)}(Exists)(By{Resource(Field)}(And{Resource(Field)}...))
This is to avoid duplication by aiding code search through "obvious" names: if
you know you need a Foo by its barBat, you should look for
fetchFooByBarBat before writing it.
-- Good
fetchTeachersBySchoolId :: SchoolId -> SqlPersistT m [Entity Teacher]
fetchTeacherIdsBySchool :: School -> SqlPersistT m [TeacherId]
-- NOTE: We don't go as far as distinguishing Entity or not in this naming, so
-- these are also Good, with Resource ~ Teacher and Resource ~ Entity School
fetchTeachersBySchoolId :: SchoolId -> SqlPersistT m [Teacher]
fetchTeacherIdsBySchool :: Entity School -> SqlPersistT m [TeacherId]
fetchStudentEmailsByFirstNameAndLastName
:: NameComponent -- ^ First name
-> NameComponent -- ^ Last name
-> SqlPersistT m [EmailAddress]
fetchStudentEmailByFirstNameAndLastName
:: NameComponent -- ^ First name
-> NameComponent -- ^ Last name
-> SqlPersistT m (Maybe EmailAddress)
fetchTeacherExistsBySchoolId :: SchoolId -> SqlPersistT m Bool
countSessionsByAssignmentId
:: forall assignment session
. Session assignment ~ session
=> Key assignment
-> SqlPersistT m Natural
-- Bad
getAnswers :: SqlReadT [Answer]
fetchTeachersBySchool :: SchoolId -> SqlPersistT m [TeacherId]
fetchTeachersCountForStudent :: Student -> SqlPersistT m Int64Use mkPersist from persistent to generate record
types corresponding to database tables. sqlSettings will
autoprefix each field with the record's name. Note that the field names given in
the persistLowerCase quasiquotation should exactly match the corresponding
column name in the database table except that the former is camelCase and the
latter is snake_case (see Postgres above). For example:
share [mkPersist sqlSettings, mkMigrate "migration"] [persistLowerCase|
User sql=users
name Text
age Natural
deriving Eq Show Ord Generic
|]will generate a data declaration that looks like this:
data User = User
{ userName :: Text
, userAge :: Natural
}
deriving (Eq, Show, Ord, Generic)Entities that are meant to appear in API requests or responses should have JSON instances that strip the prefixes:
instance ToJSON User where
toEncoding = genericToEncoding (unPrefix "user")
toJSON = genericToJSON (unPrefix "user")
instance FromJSON User where
parseJSON = genericParseJSON (unPrefix "user")This will produce the following JSON:
{ "name": "Joe"
, "age": 29
}For complicated Handler requests or responses, where we use a custom type
instead of a domain object, such types should be named {Resource}{Method}
where Resource matches the route and Method is Sentence-case.
-- Good
data SchoolGet
-- ...
instance ToJSON SchoolGet where
-- ...
getSchoolR :: SchoolId -> Handler Value
getSchoolR schoolId = do
school <- get404 schoolId
sendStatusJSON status200 SchoolGet {- ... -}
data SchoolsPost
-- ...
instance FromJSON SchoolsPost where
-- ...
postSchoolsR :: Handler Value
postSchoolsR = do
SchoolsPost {..} <- requireJsonBody
-- ...
-- Bad
data GetSchool
-- Out of order (though gramatically attractive)
data SchoolGET
-- Wrong case
data SchoolResponse
-- Missing Method, redundant suffix
data GetSchools
-- Incorrect plurization
data CreateSchoolPost
-- Duplicate verb, incorrect pluralizationJavaScript uses camelCase for identifiers and TitleCase for classes and
React components. For flow types, we generally append a T to the end of the
type, e.g. SkillT. Names that are being ignored should be prefixed with an
underscore (e.g. _teacher, _standard). It's a common mistake (especially for
Haskellers) to use a lone _ and accidentally overwrite underscore in that
scope.
JSON keys should always be camelCase. String enums that cross language
barriers should be snake_case, since most enums that cross language barriers
are designed for postgres first. Exceptions can be made for short initialisms
which may be ALLCAPS (e.g. TEKS: MathSystemT). Short initialisms that are
part of other identifiers should use snake_case (e.g.
rti_coordinator: TeacherRoleT).
Snake-case enum
data TeacherRole
= Teacher
| RtiCoordinator
| ...
-- Equivalent to
-- mkPersistEnumUsing (snakeCaseify . unCapitalize) ''TeacherRole
-- Produces
-- 'teacher'
-- 'rti_coordinator'
-- ...
mkPersistEnum ''TeacherRoleAll-caps enum
data MathSystem
= CCSS -- Common Core Standard System
| TEKS -- Texas Essential Knowledge and Skills
| ...
-- Produces
-- 'CCSS'
-- 'TEKS'
mkPersistEnumUsing id ''MathSystemSnake-case enum
data TeacherRole
= Teacher
| RtiCoordinator
| ...
-- Equivalent to
-- deriveJSONEnumUsing (snakeCaseify . unCapitalize) ''TeacherRole
-- Produces
-- "teacher"
-- "rti_coordinator"
-- ...
deriveJSONEnum ''TeacherRoleAll-caps enum
data MathSystem
= CCSS -- Common Core Standard System
| TEKS -- Texas Essential Knowledge and Skills
| ...
-- Produces
-- "CCSS"
-- "TEKS"
-- ...
deriveJSONEnumUsing id ''MathSystemJavaScript enums should be typed using flow, e.g.:
type TeacherRoleT
= 'teacher'
| 'rti_coordinator'
| ...One is encouraged to make smart constructors for enums to reduce the risk of
misspellings. flow usually catches misspellings, but not always.
const TeacherRoles = {
Teacher: ('teacher': TeacherRoleT),
RtiCoordinator: ('rti_coordinator': TeacherRoleT),
...
}
-- e.g.
const role = TeacherRoles.Teacher