Perhaps this?
library(ggplot2)
library(dplyr)
ggs <- lapply(sort(unique(mtcars$carb)), function(crb) {
ggplot(filter(mtcars, carb == crb), aes(x = wt, y = mpg, color = disp)) +
geom_point()
})
This produces six plots, which when the first two are viewed side-by-side (calling ggs[[1]] and then ggs[[2]]), we see

An alternative might be to facet the data, as in
ggplot(mtcars, aes(x = wt, y = mpg, color = disp)) +
facet_wrap(~ carb) +
geom_point()

But the literal translation of your paste(..) <- ... code into something syntactically correct, we'd use an anti-pattern in R: assign:
for (crb in sort(unique(mtcars$carb))) {
gg <- ggplot(filter(mtcars, carb == crb), aes(x = wt, y = mpg, color = disp)) +
geom_point()
assign(paste0("carb_", crb), gg)
}
Again, this is not the preferred/best-practices way of doing things. It is generally considered much better to keep like-things in a list for uniform/consistent processing of them.
Multiple IDs ... two ways:
Nested lapply:
carbs <- sort(unique(mtcars$carb))
ggs <- lapply(carbs, function(crb) {
gears <- subset(mtcars, carb == crb)$gear
lapply(gears, function(gr) {
ggplot(dplyr::filter(mtcars, carb == crb, gear == gr), aes(x = wt, y = mpg, color = disp)) +
geom_point()
})
})
Where ggs[[1]] is a list of lists. ggs[[1]][[1]] will be one plot.
split list, one-deep:
carbsgears <- split(mtcars, mtcars[,c("carb", "gear")], drop = TRUE)
ggs <- lapply(carbsgears, function(dat) {
ggplot(dat, aes(x = wt, y = mpg, color = disp)) + geom_point()
})
Here, ggs is a list only one-deep. The names are just concatenated strings of the two fields, so since we have mtcars$carb with values c(1,2,3,4,6,8) and mtcars$gear with values c(3,4,5), removing combinations without data we have names:
names(ggs)
# [1] "1.3" "2.3" "3.3" "4.3" "1.4" "2.4" "4.4" "2.5" "4.5" "6.5" "8.5"
where "1.3" is carb == 1 and gear == 3. When column names have dots in them, this might become ambiguous.