Scala.js
Scaladores - Abril de 2016
Onilton Maciel
oniltonmaciel (gmail)
@oniltonmaciel
onilton
Front-end
Back-end
Zoeira/Besteirol
WHY
WHAT
HOW
JavaScript Linguagem
Origem
- Criada em 1995 por Brendan Eich
- Construída em < 1 semana e meia
- Derivada do C. Inspiração Scheme
- JavaScript - Marketing
javascript> ["10", "10", "10", "10"].map(parseInt)
javascript> ["10", "10", "10", "10"].map(function (x) { return parseInt(x) })
[10, NaN, 2, 3] // WHAAAAAT!!??
[10, 10, 10, 10] // Finalmente
Math.max(1, true); // 1
Math.max(0, true); // 1
Math.max(1, false); // 1
Math.max(-1, true); // 1
Math.max(-1, false); // 0
'true' == true // returns false
A parte boa
Douglas Crockford
JavaScript Experiência
Como eu me sinto quando...
... vou testar o JS que acabei de escrever
Ufa, rodou! Agora é só torcer...
... para que algo ruim não aconteça
Single Page Applications
JavaScript Plataforma
Desktop Apps
NW.js (node-webkit)
Mobile
Tessel 2
http://blog.bruchez.name/2016/04/scala-on-tessel-2.html
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
getDist() {
return Math.sqrt(this.x * this.x +
this.y * this.y);
}
}
var p = new Point(3,4);
var dist = p.getDst();
alert("Hypotenuse is: " + dist);
case class Point(x: Int, y: Int) {
def dist() = math.sqrt(x * x + y * y)
}
val p = Point(3,4)
val dist = p.dist()
println(s"Hypotunese is $dist")
WHY
WHAT
HOW
O quê funciona?
-
Suporte completo a linguagem Scala
-
Interoperabilidade completa com JavaScript
-
Código tão rápido quanto escrito em JS
-
Tamanho JS final "aceitável"
-
Ciclo rápido de mudanças
Crash course
build.sbt
project/
build.sbt
index.html
src/
main/
scala/
folder/
Code.scala
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.5")
build.sbt
project/
build.sbt
index.html
src/
main/
scala/
folder/
Code.scala
// Turn this project into a Scala.js
// project by importing these settings
enablePlugins(ScalaJSPlugin)
name := "ExampleProject"
version := "0.1-SNAPSHOT"
scalaVersion := "2.11.7"
persistLauncher in Compile := true
persistLauncher in Test := false
libraryDependencies ++= Seq(
"org.querki" %%% "jquery-facade" % "1.0-RC2",
"org.scala-js" %%% "scalajs-dom" % "0.8.2"
)
build.sbt
project/
build.sbt
index.html
src/
main/
scala/
folder/
Code.scala
...
<script
type="text/javascript"
src="./target/scala-2.11/jsontocaseclass-opt.js">
</script>
<script
type="text/javascript"
src="./target/scala-2.11/jsontocaseclass-launcher.js">
</script>
...
object Main extends js.JSApp {
def main() = {
var x = 0
while(x < 10) x += 3
println(x)
// 12
}
}
src/main/scala/folder/Code.scala
sbt fastOptJS
ScalaJS.c.LMain$.prototype.main__V = (function() {
var x = 0;
while ((x < 10)) {
x = ((x + 3) | 0)
};
ScalaJS.m.s_Predef$()
.println__O__V(x)
// 12
});
sbt ~fastOptJS
sbt fullOptJS
be.prototype.main = (function() {
for (var x=0; 10>x;)
a=((a+3) | 0);
ee(); L();
}
WHY
WHAT
HOW
http://json2caseclass.cleverapps.io/
Fork
Então...
$(function(){
$("#caseclassform textarea").change(function(e){
$('#mycodeis').html(t.scala_code({code:$(e.target).val()}));
sh_highlightDocument();
});
$("#test_button").click(function(){
$('#pastejsonform textarea').val('[{"coordinates":null,"favorited":false,"truncated":false,"created_at":"Mon Sep 03 13:24:14 +0000 2012","id_str":"242613977966850048","entities":{"urls":[],"hashtags":[],"user_mentions":[{"name":"Jason Costa","id_str":"14927800","id":14927800,"indices":[0,11],"screen_name":"jasoncosta"},{"name":"Matt Harris","id_str":"777925","id":777925,"indices":[12,26],"screen_name":"themattharris"},{"name":"ThinkWall","id_str":"117426578","id":117426578,"indices":[109,119],"screen_name":"thinkwall"}]},"in_reply_to_user_id_str":"14927800","contributors":null,"text":"@jasoncosta @themattharris Hey! Going to be in Frisco in October. Was hoping to have a meeting to talk about @thinkwall if you\'re around?","retweet_count":0,"in_reply_to_status_id_str":null,"id":242613977966850048,"geo":null,"retweeted":false,"in_reply_to_user_id":14927800,"place":null,"user":{"profile_sidebar_fill_color":"EEEEEE","profile_sidebar_border_color":"000000","profile_background_tile":false,"name":"Andrew Spode Miller","profile_image_url":"http://a0.twimg.com/profile_images/1227466231/spode-balloon-medium_normal.jpg","created_at":"Mon Sep 22 13:12:01 +0000 2008","location":"London via Gravesend","follow_request_sent":false,"profile_link_color":"F31B52","is_translator":false,"id_str":"16402947","entities":{"url":{"urls":[{"expanded_url":null,"url":"http://www.linkedin.com/in/spode","indices":[0,32]}]},"description":{"urls":[]}},"default_profile":false,"contributors_enabled":false,"favourites_count":16,"url":"http://www.linkedin.com/in/spode","profile_image_url_https":"https://si0.twimg.com/profile_images/1227466231/spode-balloon-medium_normal.jpg","utc_offset":0,"id":16402947,"profile_use_background_image":false,"listed_count":129,"profile_text_color":"262626","lang":"en","followers_count":2013,"protected":false,"notifications":null,"profile_background_image_url_https":"https://si0.twimg.com/profile_background_images/16420220/twitter-background-final.png","profile_background_color":"FFFFFF","verified":false,"geo_enabled":true,"time_zone":"London","description":"Co-Founder/Dev (PHP/jQuery) @justFDI. Run @thinkbikes and @thinkwall for events. Ex tech journo, helps run @uktjpr. Passion for Linux and customises everything.","default_profile_image":false,"profile_background_image_url":"http://a0.twimg.com/profile_background_images/16420220/twitter-background-final.png","statuses_count":11550,"friends_count":770,"following":null,"show_all_inline_media":true,"screen_name":"spode"},"in_reply_to_screen_name":"jasoncosta","source":"<a href=\\"http://www.journotwit.com\\" rel=\\"nofollow\\">JournoTwit</a>","in_reply_to_status_id":null},{"coordinates":{"coordinates":[121.0132101,14.5191613],"type":"Point"},"favorited":false,"truncated":false,"created_at":"Mon Sep 03 08:08:02 +0000 2012","id_str":"242534402280783873","entities":{"urls":[],"hashtags":[{"text":"twitter","indices":[49,57]}],"user_mentions":[{"name":"Jason Costa","id_str":"14927800","id":14927800,"indices":[14,25],"screen_name":"jasoncosta"}]},"in_reply_to_user_id_str":null,"contributors":null,"text":"Got the shirt @jasoncosta thanks man! Loving the #twitter bird on the shirt :-)","retweet_count":0,"in_reply_to_status_id_str":null,"id":242534402280783873,"geo":{"coordinates":[14.5191613,121.0132101],"type":"Point"},"retweeted":false,"in_reply_to_user_id":null,"place":null,"user":{"profile_sidebar_fill_color":"EFEFEF","profile_sidebar_border_color":"EEEEEE","profile_background_tile":true,"name":"Mikey","profile_image_url":"http://a0.twimg.com/profile_images/1305509670/chatMikeTwitter_normal.png","created_at":"Fri Jun 20 15:57:08 +0000 2008","location":"Singapore","follow_request_sent":false,"profile_link_color":"009999","is_translator":false,"id_str":"15181205","entities":{"url":{"urls":[{"expanded_url":null,"url":"http://about.me/michaelangelo","indices":[0,29]}]},"description":{"urls":[]}},"default_profile":false,"contributors_enabled":false,"favourites_count":11,"url":"http://about.me/michaelangelo","profile_image_url_https":"https://si0.twimg.com/profile_images/1305509670/chatMikeTwitter_normal.png","utc_offset":28800,"id":15181205,"profile_use_background_image":true,"listed_count":61,"profile_text_color":"333333","lang":"en","followers_count":577,"protected":false,"notifications":null,"profile_background_image_url_https":"https://si0.twimg.com/images/themes/theme14/bg.gif","profile_background_color":"131516","verified":false,"geo_enabled":true,"time_zone":"Hong Kong","description":"Android Applications Developer, Studying Martial Arts, Plays MTG, Food and movie junkie","default_profile_image":false,"profile_background_image_url":"http://a0.twimg.com/images/themes/theme14/bg.gif","statuses_count":11327,"friends_count":138,"following":null,"show_all_inline_media":true,"screen_name":"mikedroid"},"in_reply_to_screen_name":null,"source":"<a href=\\"http://twitter.com/download/android\\" rel=\\"nofollow\\">Twitter for Android</a>","in_reply_to_status_id":null}]');
$('#pastejsonform').submit();
});
$("#test_button2").click(function(){
$('#pastejsonform textarea').val('{"results":[{"address_components":[{"long_name":"1600","short_name":"1600","types":["street_number"]},{"long_name":"Amphitheatre Pkwy","short_name":"Amphitheatre Pkwy","types":["route"]},{"long_name":"Mountain View","short_name":"Mountain View","types":["locality","political"]},{"long_name":"Santa Clara","short_name":"Santa Clara","types":["administrative_area_level_2","political"]},{"long_name":"California","short_name":"CA","types":["administrative_area_level_1","political"]},{"long_name":"United States","short_name":"US","types":["country","political"]},{"long_name":"94043","short_name":"94043","types":["postal_code"]}],"formatted_address":"1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA","geometry":{"location":{"lat":37.42291810,"lng":-122.08542120},"location_type":"ROOFTOP","viewport":{"northeast":{"lat":37.42426708029149,"lng":-122.0840722197085},"southwest":{"lat":37.42156911970850,"lng":-122.0867701802915}}},"types":["street_address"]}],"status":"OK"}');
$('#pastejsonform').submit();
});
});
$(function(){
$('#pastejsonform').submit(function(e){
e.preventDefault();
$('#optionzone').html('<form class="form-horizontal" id="json_analisys_zone">'
+' <h2>Json analysis</h2>'
+' <div id="alertplace"></div>'
+' <div id="classesplace"></div>'
+' <button type="submit" class=" pull-right btn btn-primary"><i class="icon-cogs"></i> re-generate</button>'
+' </form>');
$('#json_analisys_zone').submit(re_generate_scala);
var o = null;
try{
o = JSON.parse($(e.target).find('textarea').val());
}catch(e){
$('#alertplace').append(t.error({value:'The json root is invalid...'}));
return 1;
}
if(_.isArray(o)){
$('#alertplace').append(t.alert({value:'The json root is an array, only the first entity will be analyse...'}));
o = o[0];
}
if(!_.isObject(o)){
$('#alertplace').append(t.error({value:'The json root is not an object, cannot do anything for you...'}));
return 1;
}
analyse_object(o, 'r00tJsonObject');
$('#alertplace').append(t.info({value:$('#classesplace div.one_class').length+' case class generated'}));
$('input.class_name').each(function(i,ii){
maj_name({target:ii});
});
generate_scala($('#classesplace'));
$('input.class_name').change(maj_name);
$('#classesplace input').change(re_generate_scala);
});
});
// HERE CAN BE SOME CONFIG PLACE
var scala_words = ['abstract','case','catch','class','def','do','else','extends','false','final','finally','for','forSome','if','implicit','import','lazy','match','new','null','object','override','package','private','protected','return','sealed','super','this','throw','trait','try','true','type','val','var','while','with','yield'];
var scala_chars = ['-', '_'];
var scala_types = ['List', 'Type', 'Meta', 'Result'];
for(var i in scala_words){
var oname = scala_words[i];
scala_types.push(oname.charAt(0).toUpperCase() + oname.substring(1));
}
//
var analyse_object = function(o, oname){
oname = generate_name(oname);
var sign = generate_signature(o);
if($('#class_'+sign).length > 0){
// console.log('class already analyse');
}else{
var elem = $(t.one_class({oname:oname, sha: sign}));
var elem_u = elem.find('div.ul');
if(_.size(o) > 22){
$('#alertplace').append(t.error({value:'the '+ oname + ' class is exceding 22 fields, generated but it will not work, due to the Product arity limitation'}));
}
_.each(o, function(value, key, list){
var ts = "String";
var sha = "";
var list = "";
var disabled = "";
if(_.isString(value)){
ts = "String";
}
if(_.isNumber(value)){
ts = "Double";
}
if(_.isBoolean(value)){
ts = "Boolean";
}
if(_.isDate(value)){
ts = "Date";
}
if(_.isArray(value)){
if(is_value_consistent(value)){
list='List';
disabled = "disabled";
ts = generate_name(list + '['+generate_name(key)+']');
if(_.size(value) == 0){
$('#alertplace').append(t.error({value:'the '+ oname +' '+key+ ' field is an empty array : cannot analyse :-('}));
}else{
if(_.isObject(value[0])){
sha=generate_signature_collection(value);
analyse_object(value[0], key);
}else{
var vv = value[0];
var ts2 = "String"
disabled = "";
if(_.isString(vv)){
ts2 = "String";
}
if(_.isNumber(vv)){
ts2 = "Double";
}
if(_.isBoolean(vv)){
ts2 = "Boolean";
}
if(_.isDate(vv)){
ts2 = "Date";
}
ts = generate_name(list + '['+ts2+']');
}
}
}else{
$('#alertplace').append(t.error({value:'the '+ oname +' '+key+ ' field is prentending an array but not consistent'}));
}
}
if(_.isObject(value) && !_.isArray(value)){
ts = generate_name(key);
disabled = "disabled";
sha = generate_signature(value);
analyse_object(value, key);
// }
}
elem_u.append(t.one_line({name:key,typescala:ts,sha:sha,disabled:disabled,list:list, oname:oname}));
}, this);
elem.append(t.info({value:elem_u.find('.li').length+' fields'}));
$('#classesplace').append(elem);
}
};
var sanitize_var_name = function(name){
if(name.match(/[_a-zA-Z0-9]+/) == name && !_.contains(scala_words, name)){
return name;
}else{
return '`' + name + '`';
}
}
var generate_scala = function(el){
var content = "";
_.each(el.find('.one_class'), function(value, key, list){
value = $(value);
var props = _.map(value.find('.li'), function(v, k, l){
var v = $(v);
var sst = v.find('input.typescala').val();
if ( v.find('input.optional_value[type="checkbox"]').prop("checked") ){
sst = 'Option['+sst+']';
}
return ' ' + sanitize_var_name(v.find('label.keyname').text()) + ': ' + sst;
}, this);
content += t.one_scala_cclass({cname:value.find('input.class_name').val(), ccontent: props.join(',\n')}) + '\n';
}, this);
$('#caseclassform textarea').val(content);
$('#mycodeis').html(t.scala_code({code:content}));
sh_highlightDocument();
};
var maj_name = function(e){
var elem = $(e.target);
var tochange = $('div.ul input[data-signature-class="'+elem.attr('data-signature-class')+'"]');
tochange.filter('input[data-list=""]').val(elem.val());
tochange.filter('input[data-list="List"]').each(function(i){
var ee = $(this);
ee.val(ee.attr('data-list')+'['+elem.val()+']');
});
tochange.filter('input[data-list="Map"]').each(function(i){
var ee = $(this);
ee.val(ee.attr('data-list')+'[Map,'+elem.val()+']');
});
};
var re_generate_scala =function(e){
e.preventDefault();
generate_scala($('#classesplace'));
};
var is_value_consistent = function(o){
if(_.size(o) == 0){
return true;
}else{
if(!_.isArray(o)){
o = _.values(o);
}
var n = o[0];
var nn = (_.isObject(n) ? generate_signature(n) : typeof n);
return _.every(o, function(n){ return (_.isObject(n) ? generate_signature(n) : typeof n) == nn; }, this);
}
};
var generate_signature_collection =function(o){
if(_.size(o) == 0){
return 0;
}else{
if(!_.isArray(o)){
o = _.values(o);
}
return generate_signature(o[0]);
}
};
var generate_signature =function(o){
if(_.isObject(o)){
return SHA1(_.map(_.keys(o), function(n){ return n.toLowerCase(); }).sort().join('|'));
}else{
return SHA1(_.map(o, function(n){ return typeof n; }).sort().join('|'));
}
};
var generate_name = function(oname){
var n = (oname.charAt(0).toUpperCase() + oname.substring(1));
if(_.contains(scala_types, n)){
n += 'Bis';
}
return n;
};
var t = {
alert : _.template('<div class="alert">'
+'<button type="button" class="close" data-dismiss="alert">×</button>'
+'<i class="icon-warning-sign"></i> <%= value %>'
+'</div>'),
error : _.template('<div class="alert alert-error">'
+'<button type="button" class="close" data-dismiss="alert">×</button>'
+'<i class="icon-warning-sign"></i> <%= value %>'
+'</div>'),
info : _.template('<div class="alert alert-info">'
+'<button type="button" class="close" data-dismiss="alert">×</button>'
+'<%= value %>'
+'</div>'),
one_line : _.template('<div class="li control-group">'
+'<label class="keyname control-label"><%= name %></label> '
+'<div class="controls">'
+'<div class="input-append"><input class="typescala" <%= disabled %> type="text" data-signature-class="<%= sha %>" data-list="<%= list %>" value="<%= typescala %>" />'
+' <span class="add-on"><input class="optional_value" type="checkbox" value="" id="chkb_<%= oname %>_<%= name %>" /><label class="label_chkbr" for="chkb_<%= oname %>_<%= name %>"> optional</label></span>'
+'</div>'
+'</div>'
+'</div>'
+''),
one_class : _.template('<div id="class_<%= sha %>" class="one_class">'
+'<fieldset>'
+'<div class="class_title"><i class="icon-leaf"></i> <input class="class_name" data-signature-class="<%= sha %>" type="text" value="<%= oname %>" /></div>'
+'<div class="ul"></div>'
+'</fieldset>'
+'</div>'),
one_scala_cclass : _.template('case class <%= cname %>(\n<%= ccontent %>\n)'),
one_scala_props : _.template('<%= pname %>\:<%= ptype %>'),
scala_code : _.template('<pre class="sh_scala"><%= code %></pre>')
};
sed 's/;$//g'
Tipos em todo lugar!
function -> def
var sanitize_var_name = function(name) {
if(name.match(/[_a-zA-Z0-9]+/) == name && !_.contains(scala_words, name)){
return name;
}else{
return '`' + name + '`';
}
}
def sanitize_var_name(name: String): String = {
/* java's String.matches all input data */
if (name.matches("[_a-zA-Z0-9]+") && !scala_words.contains(name)) {
return name
} else {
return '`' + name + '`'
}
}
var generate_scala = function(el){
var content = "";
_.each(el.find('.one_class'), function(value, key, list){
value = $(value);
var props = _.map(value.find('.li'), function(v, k, l){
var v = $(v);
var sst = v.find('input.typescala').val();
if ( v.find('input.optional_value[type="checkbox"]').prop("checked") ){
sst = 'Option['+sst+']';
}
return ' ' + sanitize_var_name(v.find('label.keyname').text()) + ': ' + sst;
}, this);
content += t.one_scala_cclass({cname:value.find('input.class_name').val(), ccontent: props.join(',\n')}) + '\n';
}, this);
$('#caseclassform textarea').val(content);
$('#mycodeis').html(t.scala_code({code:content}));
sh_highlightDocument();
};
def generate_scala(el: ???): Unit = {
....
}
x: js.Dynamic
var generate_scala = function(el){
var content = "";
_.each(el.find('.one_class'), function(value, key, list){
value = $(value);
var props = _.map(value.find('.li'), function(v, k, l){
var v = $(v);
var sst = v.find('input.typescala').val();
if ( v.find('input.optional_value[type="checkbox"]').prop("checked") ){
sst = 'Option['+sst+']';
}
return ' ' + sanitize_var_name(v.find('label.keyname').text()) + ': ' + sst;
}, this);
content += t.one_scala_cclass({cname:value.find('input.class_name').val(), ccontent: props.join(',\n')}) + '\n';
}, this);
$('#caseclassform textarea').val(content);
$('#mycodeis').html(t.scala_code({code:content}));
sh_highlightDocument();
};
def generate_scala(el: js.Dynamic): Unit = {
var content = "";
//....
}
calculaSalario(el.find('input.salario').text().asInstanceOf[Float])
No começo, abrace o asInstanceOf[T]
Deixe os returns em paz
js.Dynamic.global
// Original JS
SHA1("testestring")
// Scala
js.Dynamic.global.SHA1("testestring")
// Shorter version
import js.Dynamic.{global => g}
g.SHA1("testestring")
// Underscore.js - Original JS
_.size(lista)
// Scala
g.`_`.size(lista) // js.Dynamic
val size = g.`_`.size(lista).asInstanceOf[Int]
Facade
@js.native
trait Underscore extends js.Object {
def size(x: js.Dynamic): Int = js.native
}
@JSName("_")
object _u extends Underscore
val size = _u.size(lista)
var generate_scala = function(el){
var content = "";
_.each(el.find('.one_class'), function(value, key, list){
value = $(value);
var props = _.map(value.find('.li'), function(v, k, l){
var v = $(v);
var sst = v.find('input.typescala').val();
if ( v.find('input.optional_value[type="checkbox"]').prop("checked") ){
sst = 'Option['+sst+']';
}
return ' ' + sanitize_var_name(v.find('label.keyname').text()) + ': ' + sst;
}, this);
content += t.one_scala_cclass({cname:value.find('input.class_name').val(), ccontent: props.join(',\n')}) + '\n';
}, this);
$('#caseclassform textarea').val(content);
$('#mycodeis').html(t.scala_code({code:content}));
sh_highlightDocument();
};
def generateScala(el: JQuery): Unit = {
val content = el.find(".one_class").mapElems { value =>
val jvalue = $(value)
val props = jvalue.find(".li").mapElems { v =>
val jv = $(v)
val sst = jv.find("input.typescala").valueString
val finalSst = if (jv.find("""input.optional_value[type="checkbox"]""").prop("checked").orNull.asInstanceOf[Boolean]) {
s"Option[$sst]"
} else sst
" " + sanitizeVarName(jv.find("label.keyname").text()) + ": " + finalSst
}
t.oneScalaCClass(jvalue.find("input.class_name").valueString, props.mkString(",\n"))
}.mkString("\n")
$("#caseclassform textarea").value(content)
$("#mycodeis").html(t.scalaCode(content))
g.sh_highlightDocument()
}
Conclusões
- Scala.js está pronto
- No dia-a-dia, funciona como Scala na JVM, com pouquíssimas diferenças e limitações
- A maior dificuldade é a integração com JS existente, tem uma pequena curva de aprendizado, mas muito relacionado ao DOM.
- Há muitas facades prontas, mas construir sua própria não é tão difícil
- Iniciantes, comecem com Scala
Referências e recursos
- https://www.scala-js.org
- https://github.com/sjrd/scala-js-example-app
- http://www.lihaoyi.com/hands-on-scala-js/
- http://ochrons.github.io/scalajs-spa-tutorial/
- https://gitter.im/scala-js/scala-js
Scalajs - Scaladores Abril 2016
By Onilton Maciel
Scalajs - Scaladores Abril 2016
- 744