General purpose programming language
- imperative (sequence of stateful, ordered steps that describe how to do something)
- wide scope
- Java, Bash etc
- potential for "runtime errors"
DSL (Domain Specific Language)
- declarative (describes what something is, not how to do it)
- deliberately limited scope
- HTML, CSS, SQL, Maven or Gradle build files etc
- reduced potential for "runtime errors" - invalid structures can be disallowed
These seem to be terms made up by the authors.
Relative term. SQL is a DSL, but from the perspective of eg Kotlin, it is an "external" DSL as it's not, well, Kotlin, and thus has to be represented as String.
interface AccountDao {
@SqlQuery("""
SELECT
account_id,
user_id,
status,
created_timestamp,
modified_timestamp
FROM
account
WHERE
user_id = :userId
""")
@RegisterRowMapper(AccountRowMapper::class)
@Throws(JdbiException::class)
fun get(userId: UserId): Account?
}
The IDE can still help out with syntax and indentation (either out of the box or using plugins) but it would be better if SQL wasn't represented as String.
Again, relative term; SQL is internal to SQL
SELECT
account_id,
user_id,
status,
created_timestamp,
modified_timestamp
FROM
account
WHERE
user_id = 123
But while DSL is an external DSL to Kotlin, it can be turned into an internal one! Eg
class AccountDao {
fun get(userId: UserId) : Account? {
//this is actual QueryDSL, a library that
//turns SQL into an internal DSL for JVM
queryFactory
.selectFrom(account)
.where(
account
.userId
.eq(userId))
.fetch() //List<Account>
.findFirst() //Account?
}
}
Note: the whole get function is just regular Kotlin!
It's not a String, it doesn't use any weird special syntax for replacing :userId that is neither SQL nor Kotlin.
The Kotlin compiler is leveraged to disallow invalid queries like
class AccountDao {
fun get(userId: UserId) : Account? {
queryFactory
.selectFrom(account)
.where(
person
.userId
.eq(userId))
.fetch() //List<Account>
//^^^^ doesn't compile, types don't line up
}
}
Importantly, while it's just regular Kotlin, it's also very close to regular SQL. So QueryDSL is an excellent example of an internalized DSL that respects both the target and the host languages.
Another very similar library is JOOQ, which looks more or less the same but as far I understand unlike QueryDSL is paid.
create
.select(BOOK.TITLE)
.from(BOOK)
.where(BOOK.PUBLISHED_IN.eq(2011))
.orderBy(BOOK.TITLE)
The book goes through a bunch of techniques used for achieving "clean" syntax DSLs:
For overachievers
StringUtil.capitalized(s);
s.capitalized();
Agree, that's a method that really should be on String and a good use of an extension function.
For overachievers
1.to("one")
1 to "one"
Disagree. While 1 to "one"
is actually Kotlin, to me it reads funny, which at least to me kind of defeats the purpose of the internal DSL reading like regular vanilla syntax in that language.
For overachievers
set.add(2)
set += 2
Disagree. Again, it doesn't read like Kotlin and it's not at all intuitively clear what += would do to a set.
For overachievers
someMap.get("foo")
someMap["foo"]
Disagree. For one, ["foo"]
kind of looks like a JSON array with single item "foo"
. Or is it a boolean for whether the map contains the key "foo"
, or maybe whether the map contains the value "foo"
. Or is it a get? Again, if it is, does it get the entry or the value?
For overachievers
// fun getUser(id: UserId) : Either<Failure, User>
getUser(userId)
.fold({ failure ->
when (failure) {
DB_ERROR -> Response.status(500)
NOT_FOUND -> Response.status(404)
}
}, { user ->
Response.success(user)
})
// fun getUser(id: UserId) : Either<Failure, User>
getUser(userId)
.fold({
when (it) {
DB_ERROR -> Response.status(500)
NOT_FOUND -> Response.status(404)
}
}, {
Response.success(it)
})
Disagree. In nested such statements, it's not clear which it
it
refers to. The compiler does prevent unintended use of the wrong it
at the wrong level of nesting IF the types are different, but it
(pun intended) is a source of errors when the types are the same and the wrong thing is referred to at the wrong level.
For overachievers
sb.append("yes")
sb.append("no")
with (sb) {
append("yes")
append("no")
}
Can't think of anything to say about this one.
As commented, while many or all of these techniques can be used, they also lend themselves to abuse. In the book there is an example of Exposed, an internal DSL tool that competes with QueryDSL and JOOQ
val result = (Country join Customer).select { Country.name eq "USA" }
result.foreach { println(it[Customer.name]) }
It somehow manages to not only use a hodgepodge of (), {} AND [], it also doesn't read as neither Kotlin nor SQL.
The book haz the explains
val result = (Country join Customer)
.select { Country.name eq "USA" } //Corresponds to this SQL code: WHERE country = "USA"
result.foreach { println(it[Customer.name]) }
I have no fucking idea what that does or how it does it. (OK, after a brief think, actually I do have an idea what it does, but it doesn't feel intuitive or idiomatic)
When done well, internal DSLs can be incredibly useful and valuable, increasing clarity and performance, and reducing bugs. When done poorly, they're worse than just biting the bullet and sticking to eg representing SQL as String.
The ability to create internal DSLs isn't unique to Kotlin- Java and countless other languages already have countless internal DSL tools. It's not even any more or less easy or hard to create internal DSLs in Kotlin compared to any other language.
The overachievers amongst you who do look into the specifics of the techniques used will find that they are complex and that internal DSLs require non-trivial effort to create and maintain. Eg, writing an internal DSL that supports trivial SQL SELECT FROM WHERE
statements may not seem like that that much work, but once you take into account more advanced queries it quickly becomes a lot of work. And SQL isn't even really that big of a domain!
So internal DSL tools are projects in their own right, and it's unlikely that you will find yourself creating ad hoc internal DSLs in Kotlin, just like in any other language. The lowest hanging fruit for the common use cases has already been picked in the form of widely used, well tested and actively maintained tools that are highly likely to be much better than anything you can come up with, and are certain to require less effort. (just pull in the dependency and start using it)